UI의 역할은 화면에 어플리케이션 데이터를 표시하고 사용자 상호작용의 기본 지점으로서의 역할을 수행하는 것.

사실상 UI는 데이터 레이어에서 가져온 어플리케이션의 상태를 시각적으로 나타낸다.

하지만 일반적으로 데이터 레이어에서 가져오는 어플리케이션 데이트는 표시해야 하는 정보와 다른 형식

-> UI용으로 데이터의 일부만 필요

-> 사용자에게 관련성 있는 정보를 표시하기 위해 서로 다른 두 데이터 소스를 병합

UI 레이어는 어플리케이션 데이터 변경사항을 UI가 표시할 수 있는 형식으로 변환한 후에 표시하는 파이프라인이다.

 

UI Layer Architecture

UI라는 용어는 사용하는 API(View or Jetpack Compose)와 관계없이 데이터를 표시하는 Activity or Fragment와 같은 UI 요소를 가리킨다. 

Data Layer의 역할은 앱 데이터를 보유하고 관리하며 앱 데이터에 엑세스할 권한을 제공하는 것이므로 UI 레이어에서 다음 단계를 실행한다.

1. 앱 데이터를 사용하고 UI에서 쉽게 렌더링할 수 있는 데이터로 변환한다.

2. UI 렌더링 가능 데이터를 사용하고 사용자에게 표시할 UI 요소를 변환한다.

3. 이렇게 조합된 UI 요소의 사용자 입력 이벤트를 사용하고 입력 이벤트의 결과를 필요에 따라 UI 데이터에 반영한다.

4. 1~3단계를 필요에 따라 반복

 

UI State Definition

간단히 말해 UI에는 각 기사의 일부 MetaData와 함께 기사 목록이 표시된다. 

즉, 사용자가 보는 화면이 UI라면 UI State는 앱에서 사용자가 봐야 한다고 지정하는 항목이다.

UI State 변경 -> UI 즉시 반영

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

불변성

위 예에서 UI State 정의는 변경할 수 없다. 불변성의 주요 이점은 변경 불가능한 객체가 순간의 어플리케이션 상태를 보장한다는 점이다.

이로 인해 UI는 상태를 읽고 이에 따라 UI 요소를 업데이트하는 한 가지 역할에 집중할 수 있다. 따라서 UI 자체가 데이터의 유일한 소스인 경우를 제외하고 UI에서 UI State를 직접 수정해서는 안된다. 이 원칙을 위반하면 동일한 정보가 여러 정보 소스에서 비롯되어 데이터 불일치와 미세한 버그가  발생한다.

 

상기 가이드의 이름 지정 규칙은 다음과 같다.

기능 + UIState

예를 들어 뉴스를 표시하는 화면의 상태는 NewsUiState, 뉴스 항목 목록에 있는 뉴스 항목의 상태는 NesItemUiState일 수 있다.

 

단방향 데이터 흐름으로 UI State 관리

앞서 상기 UI State가 UI Rendering에 필요한 세부정보가 포함된 변경 불가능한 Snapshot임을 확인했다.

하지만 앱 데이터의 동적 특성에 따라 상태는 시간이 지나면서 변경될 수 있으며 이는 앱을 채우는 데 사용되는 기본 데이터를 수정하는 사용자 상호작용이나 기타 이벤트로인해 발생하기도 한다.

단방향 데이터 흐름(Unidirectional Data Flow)

 

상태 홀더

UI State를 생성하는 역할을 담당하고 생성 작업에 필요한 로직을 포함하는 클래스를 상태 홀더(State Holder)라고 한다.

전체 화면이나 탐색 대상의 경우 일반적인 구현은 ViewModel의 인스턴스이지만 어플리케이션의 요구사항에 따라 간단한 클래스로도 충분하다. 

 

UI 와 State 생성자 간의 상호 종속을 모델링하는 방법은 다양하다. 하지만 UI와 ViewModel클래스 사이의 상호작용은 대체로 이벤트 입력과 입력의 후속 상태인 출력으로 간두될 수 있으며 관계는 상기 다이어그램과 같다.

상태가 아래로 향하고 이벤트는 위로 향하는 패턴을 단방향 데이터 흐름(UDF)이라고 한다. 이 패턴이 앱 아키텍처에 미치는 영향은 다음과 같다.

1. ViewModel이 UI에 사용될 상태를 보유하고 노출한다. UI State는 ViewModel에 의해 변환된 어플리케이션 데이터이다.

2. UI가 ViewModel에 사용자 이벤트를 알린다. 

3. ViewModel이 사용자 작업을 처리하고 State(상태)를 업데이트한다.

4. 업데이트된 상태가 렌더링할 UI에 다시 제공된다.

5. 상태 변경을 야기하는 모든 이벤트 위의 작업이 반복된다.

 

상기 다이어그램을 보자. 사용자의 기사 북마크 요청은 상태 변경을 야기할 수 있는 이벤트의 예이다. 상태 생성자의 경우 UI State의 모든 필드를 채우고 UI가 완전히 렌더링되는데 필요한 이벤트를 처리하기 위해 모든 필수 로직을 정의하는 역할은 ViewModel이 담당한다.

로직의 유형

상기 예처럼 기사 북마크는 앱에 가치를 부여하므로 비즈니스 로직의 예이다. 

-> 비즈니스 로직은 State 변경에 따라 진행해야 할 작업이다. 비즈니스 로직은 일반적으로 도메인 또는 데이터 레이어에 배치되지만 UI Layer에는 배치되지 않는다.

-> UI 동작 로직 또는 UI 로직은 State 변경 사항을 표시하는 방법이다. 

* 특히 Context와 같은 UI 유형의 경우 UI 로직은 ViewModel이 아닌 UI에 있어야 한다. 테스트 가능성을 높이고 문제 구분에 도움이 되도록 UI로직을 다른 클래스에 위임하고자 하며 UI가 점점 복잡해지는 경우 간단한 클래스를 State Holder로 만들 수 있다.

 

UDF(Unidirectional Data Flow)을 사용하는 이유

UDF는 상기 다이어그램과 같이 상태 생성 주기를 모델링한다. 또한 상태 변경이 발생하는 위치, 변환되는 위치, 최종적으로 사용되는 위치를 구분한다. 이렇게 구분하면 UI가 이름에 드러난 의미 그대로 동작할 수 있다.

즉, 상태 변경사항을 관찰하여 표시하고 변경사항을 ViewModel에 전달하여 사용자 인텐트를 전달한다.

UDF 를 사용하면 다음이 가능하다.

1. 데이터 일관성: UI용 정보 소스가 하나이다.

2. 테스트 가능성: 상태 소스가 분리되므로 UI와 별개로 테스트 할 수 있다.

3. 유지 관리성: 상태 변경은 잘 정의된 패턴을 따른다. 즉, 변경은 사용자 이벤트 및 데이터를 가져온 소스 모두의 영향을 받는다.

 

UI 상태 노출

UI 상태를 정의하고 이 상태의 생성을 관리할 방법을 결정한 후에는 생성된 상태를 UI에 표시하는 단계를 진행한다.

UDF를 사용하여 상태 생성을 관리하므로 생성된 상태를 스트림으로 간주할 수 있다.

즉, 시간 경과에 따라 여러 버전의 상태가 생성된다. 따라서 LiveData, StateFlow와 같이 관찰 가능한 데이터 홀더에 UI 상태를 노출해야 한다. 이유는 ViewModel에서 데이터를 직접 가져오지 않고도 UI가 상태 변경사항에 반응할 수 있도록 하기 위해서이다.

이러한 유형은 항상 최신 버전의 UI 상태를 캐시한다는 이점도 있다. 이는 구성 변경 후 빠른 상태 복원에 유용하다.

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = …
}

UI에 노출되는 데이터가 비교적 간단할 때는 UI 상태 유형으로 데이터를 래핑하는 것이 좋은 경우가 많다.

내보낸 상태 홀더와 관련 View/UI Component 간의 관계를 전달하기 때문. 또한 UI Component가 더 복잡해질 때 언제나 간편하게 UI State Definition을 추가하여 UI Component를 렌더링하는데 필요한 더 많은 정보를 포함할 수 있다.

 

UIState Stream을 만드는 일반적인 방법은 ViewModel에서 지원되는 변경 가능한 스트림을 변경 불가능한 스트림으로 노출하는 것이다.

Ex) MutableStateFlow<UIState> 를 StateFlow<UIState>로 노출한다.

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

이후, ViewModel은 상태를 내부적으로 변경하는 메서드를 노출하여 UI에 사용되도록 업데이트를 게시한다. 예를 들어 비동기 작업을 실행해야 하는 경우 viewModelScope를 사용하여 코루틴을 실행하고 코루틴이 완료되면 변경 가능한 상태를 업데이트 할 수 있다.

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

상기 예에서 NewsViewModel 클래스는 특정 카테고리의 기사를 가져오려고 시도한 후에 결과에 따라 UI가 적절하게 반응할 수 있도록 시try catch결과를 UI 상태에 반영한다. 

 

추가 고려사항

1. UI State 객체는 서로 관련성 있는 State를 처리해야 한다. 이렇게 하면 불일치가 줄어들고 코드를 이해하기 더 쉽다. 뉴스 항목 목록과 북마크 수를 서로 다른 두 스트림에 노출하면 한 스트림이 업데이트 되고 다른 스트림은 업데이트되지 않은 상황이 발생할 수 있다. 단일 스트림을 사용하면 두 요소가 최신 상태로 유지된다. 또한 일부 비즈니스 로직에는 소스 조합이 필요할 수 있다.

Ex) 로그인한 상태인 동시에 프리미엄 뉴스 서비스의 구독자인 사용자에게만 북마크 버튼을 표시해야 한다면 다음과 같이 UI State클래스를 정의하면된다.

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf()
)

val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

2. UI State: 단일 스트림인지 여러 스트림인지. UI State 노출 대상을 단일 스트림과 여러 스트림 중에서 선택할 때 주요 원칙은 항목 간의 관계이다. 단일 스트림 노출의 가장 큰 장점은 편의성과 데이터 일관성이다. 즉, State 사용자가 언제나 즉시 최신 정보를 확인할 수 있다. 하지만 ViewModel 상태의 스트림이 별개일 때 적합한 경우가 있다.

-> 관련 없는 데이터 유형: UI를 렌더링하는 데 필요한 일부 상태는 서로 완전히 별개일 수 있다. 이때 서로 다른 상태를 번들로 묶는 데 드는 비용이 이점보다 더 클 수 있으며 이는 상태 중 하나가 다른 상태보다 더 자주 업데이트 되는 경우에 특히 그렇다.

-> UIState 비교(diff): UIState 객체에 필드가 많을수록 필드 중 하나를 업데이트하면 스트림이 내보내질 가능성이 크다. 뷰에는 연속적으로 이러우지는 내보내기가 같은지 다른지 파악하는 비교(diff) 메커니즘이 없으므로 내보내기할 때마다 뷰가 업데이트된다. 따라서 Flow API 또는 LiveData의 distinctUntilChaged()와 같은 메서드를 사용한 완화 작업이 필요할 수있다.

 

UI State 사용

UI에서 UIState 객체의 스트림을 사용하려면 사용 중인 관찰 가능한 데이터 유형에 터미널 연산자를 사용한다.

Ex) LiveData -> observe(), Kotlin Flow -> collect()

UI에서 관찰 가능한 데이터 홀더를 사용할 때는 UI의 수명 주기를 고려해야 한다. 수명 주기를 고려해야 하는 이유는 사용자에게 뷰가 표시되지 않을 때 UI가 UI 상태를 관찰해서는 안되기 때문. LiveData를 사용하면 LifecycleOwner가 수명 주기 문제를 암시적으로 처리한다. Flow를 사용할 때는 적절한 코루틴 범위와 repeatOnLifecycle API로 처리하는 것이 가장 좋다.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

진행 중인 작업 표시

UIState 클래스의 로드 상태를 나타내는 간단한 방법은 Boolean Field를 사용하는 것이다.

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

이 플래그의 값은 UI에 진행률 표시줄이 존재하는지를 나타낸다.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

화면에 오류 표시

UI에서의 오류 표시는 진행 중인 작업 표시와 비슷하다. 두 작업은 모두 존재 여부를 나타내는 Boolean 값으로 쉽게 표현되기 때문이다.

하지만 오류에는 사용자에게 다시 전달하는 관련 메시지 또는 실패한 작업을 다시 시도하는 관련 작업이 포함될 수 있다.

따라서 진행중인 작업을 로드하고 있거나 로드하고 있지 않은 동안 오류 컨텍스트에 적절한 메타데이터를 호스팅하는 데이터  클래스를 사용하여 오류 상태를 모델링해야 할 수 있다.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

Threading and concurrency

ViewModel에서 실행되는 모든 작업은 기본 스레드에서 안전하게 호출된다는 기본 안전성을 갖추어야 한다. 데이터 레이어와 도메인 레이어가 작업을 다른 스레드로 옮기는 역할을 담당하기 때문.

또한 장기 실행 작업의 경우 ViewModel에서 로직을 백그라운드 스레드로 옮기는 역할을 담당한다. Kotlin 코루틴은 동시 실행 작업을 관리하는 좋은 방법이며, JetPack 아키텍처 구성요소는 Kotlin Coroutine을 기본적으로 지원한다.

Navigation

앱 Navigation 변경사항은 주로 이벤트 같은 내보내기에 의해 이루어진다. 예를 들어 SignInViewModel Class가 로그인을 실행하면 UIState는 isSignedin 필드를 true로 설정할 수 있다. 이러한 Trigger 위의 UI State 사용에서 설명한 대로 사용해야 한다.

단, 사용 구현이 탐색 구성요소를 지연해야 한다.

Paging

Paging 라이브러리는 UI에서 PagingData라는 유형과 함께 사용된다. PagingData는 시간 경과에 따라 변경될 수 있는(즉, 변경 불가능한 유형이 아님)항목을 나타내고 포함하므로 변경 불가능한 UI 상태로 표현되엇는 안된다. 대신 ViewModel의 데이터를 독립적으로 자체 스트림에서 노출해야한다.

Animation

부드럽고 원활한 최상위 탐색 전환을 제공하기 위해 다음 화면의 데이터가 로드될 때까지 기다린 후에 애니메이션을 시작하는 것이 좋다.

Android View Framework는 postponeEnterTransition() 및 startPostponedEnterTransition() API를 사용하여 프래그먼트 대상 간의 전환을 지연하는 후크를 제공한다.

면접을 압두고, 앱 아키텍처에 대해 다시한번 공부해야겠다 생각하여 작성한다. 

참고는 물론 안드로이드 개발자 문서.

 

모바일 앱 사용자 환경

Android App component: Activity, Fragment, Service, Api, broadcast receiver 등 여러 앱 구성 요소가 포함된다.

-> 이것들을 menifest에서 선언하며 OS에서는 상기 파일들을 사용해 전반적인 사용자 경험에 앱을 통합한다.

 

환경조건: 

1. 사용자는 짧은 시간 내에 여러 앱과 상호작용하는 경우가 있다. -> 즉, 헤비유저같은..?

2. 휴대기기는 리소스가 제한되어 있으므로, 운영체제에서 새로운 앱을 위한 공간을 확보하도록 언제든지 일부 앱 프로세스를 종료해야할 수 있다.

 

 이러한 이벤트는 직접 제어할 수 없기 때문에 앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 되며 앱 구성요소가 서로 종속되면 안 됩니다.

 

일반 아키텍처 원칙

Android 앱은 크기가 커지기 때문에 앱을 확장하고 앱의 견고성을 높이며 앱을 더 쉽게 테스트 할 수 있도록 아키텍처를 정의해야한다.

앱 아키텍처는 앱의 부분과 그 각 부분에 필요한 기능 간의 경계를 정의한다.

준수해야 할 특정원칙은 다음과 같다.

 

1. 관심사 분리

Activity 또는 Fragment에 모든 코드를 작성하는건 굉장히 큰 실수. 이러한 UI 기반 클래스는 UI 및 운영체제 상호작용을 처리하는 로직만 포함

2. 데이터 모델에서 UI 도출하기

데이터 모델이란 앱의 데이터를 나타내며, 앱의 UI 요소 및 기타 구성요소로부터 독립되어 있다. 즉, 이들은 UI 및 앱 구성요소 수명주기와는 관련이 없다. 하지만, OS가 메모리에서 앱의 프로세스를 삭제하기로 결정하면 데이터 모델도 삭제된다.

 

이상적인 지속 모델이 중요한 이유

 -> Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터가 삭제되지 않는다.

 -> 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동한다.

 

권장 앱 아키텍처

UI Layer

UI Layer(또는 프레젠테이션 레이어)의 역할은 화면에 어플리케이션 데이터를 표시하는 것이다.

사용자 상호작용(버튼 액션) 또는 외부 입력(네트워크 응답)으로 인해 데이터가 변할 때마다 변경사항을 반영하도록 UI가 업데이트 되어야 한다.

UI 레이어는 다음 두 가지로 구성된다.

1. 화면에 데이터를 렌더링하는 UI 요소, 이러한 요소는 뷰 또는 Jetpack Compose 함수를 사용하여 빌드할 수 있다.

2. 데이터를 보유하고 이를 UI에 노출하며 로직을 처리하는 상태 홀더(ex: ViewModel)

Data Layer

앱의 데이터 레이어에는 비즈니스 로직이 포함되어 있다. 비즈니스 로직은 앱에 가치를 부여하는 요소로, 앱의 데이터 생성, 저장, 변경 방식을 결정하는 규칙으로 구성된다.

데이터 레이어는 각각 0개부터 여러 개의 데이터 소스를 포함할 수 있는 저장소로 구성된다. 앱에서 처리하는 다양한 유형의 데이터별로 저장소 클래스를 만들어야 한다.

예) 영화 관련 데이터에는 MoviesRepository Class, 결제 관련 데이터에는 PaymentRepository 클래스를 만들 수 있다.

 

저장소 클래스에서 담당하는 작업은 다음과 같다.

1. 앱의 나머지 부분에 데이터 노출

2. 데이터 변경사항을 한 곳에 집중

3. 여러 데이터 소스 간의 충돌 해결

4. 앱의 나머지 부분에서 데이터 소스 추상화

5. 비즈니스 로직 포함

각 데이터 소스 클래스는 파일, 네트워크 소스, 로컬 데이터베이스와 같은 하나의 데이터 소스만 사용해야 한다. 데이터 소스 클래스는 데이터 작업을 위해 어플리케이션과 시스템 간의 가교 역할을 한다.

 

Domain Layer(Optional)

Domain Layer는 복잡한 비즈니스 로직, 또는 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다. 모든 앱에 이러한 요구사항이 있는 것이 아니므로 이 레이어는 선택사항이다. 따라서 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 사용.

 

구성요소 간 종속성 관리

특정 클래스의 종속 항목을 수집하는 데 다음 디자인 패턴 중 하나를 사용할 수 있다.

-> 종속 항목 주입(DI): 종속 항목 주입을 사용하면 클래스가 자신의 종속 항목을 구성할 필요 없이 종속 항목을 정의할 수 있다.

-> 서비스 로케이터: 서비스 로케이터 패턴은 클래스가 자신의 종속 항목을 구성하는 대신 종속 항목을 가져올 수 있는 레지스트리를 제공.

*이 패턴은 코드를 중복학나 복잡성을 추가하지 않아도 종속 항목을 관리하기 위한 명확한 패턴을 제공하므로 코드를 확장할 수 있다.

또한 이러한 패턴을 사용하면 테스트와 프로덕션 간에 신속하게 전환할 수 있다.

 

종속 항목 삽입 패턴을 따르고 Android 앱에서 Hilt 라이브러리를 사용하는 것이 좋다.

-> Hilt는 종속 항목 트리를 따라 이동하여 객체를 자동으로 구성하고 종속 항목의 컴파일 시간을 보장하며 Android 프레임워크 클래스의 종속 항목 컨테이너를 만든다.

 

 

RecyclerView Item

다음과 같이 각 아이템들로 구성된 리사이클러 뷰가 있다고 가정하자.

이후, 아이템 별 데이터를 세팅해주고 클릭 버튼을 누른 후 스크롤을 하면 데이터가 사라진다.

이렇게 되었을때 해결 방법 중 하나는 다음과 같다.

Adapter 내에 데이터가 많이 들어가 있을 경우 임시 저장  HashMap<Int, Object>를..

단순 CheckButton일 경우, SparseBooleanArray를 사용하면된다.

이는 들어오는 데이터 종류에 따라 자유롭게 사용하면된다.

 

백그라운드에서 어떻게 돌아가는지 생각해보자.

본 작성자는 Activity(TabLayout) -> Fragments(RecyclerView in Fragment) & Adapter 로 구성되어있다.

Activity가 꺼지지 않는 이상(onDestroy) Fragment 및 Adapter는 살아있다고 볼 수 있다.

그러므로 Adapter 내에 전역변수로 위 임시 저장 자료형을 선언해주고 데이터 변경 이벤트에 따라 itemPosition, Data를 넣어주면 된다.

이후 다른 탭을 가거나 스크롤을해도 문제 없어진다.

package com.Health.health.Trainer.UserListPackage.ProfilePack.ExerciseRequestPack.RequestExercise.RequestingExercise.Anaerobic

import android.graphics.Color
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.Health.health.ModulePackage.ExercisePack.ExerciseModule
import com.Health.health.Model.TrainerDataModel
import com.Health.health.R
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions

class RequestAbsAdapter : RecyclerView.Adapter<RequestAbsAdapter.ViewHolder>(){
    var exerciseModels = mutableListOf<TrainerDataModel.RequestingExerciseData>()
    var savedList = mutableListOf<TrainerDataModel.RequestingFinalExerciseData>()
    private val tempList = hashMapOf<Int, TrainerDataModel.TempRequestingData>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RequestAbsAdapter.ViewHolder {
        val view: View? = LayoutInflater.from(parent.context).inflate(R.layout.anaerobic_item, parent, false)
        return ViewHolder(view!!)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(exerciseModels[position], position)
    }

    override fun getItemCount(): Int = exerciseModels.size

    override fun getItemViewType(position: Int): Int = position


    interface OnItemClickListener{
        fun onItemClick(v: View, position: Int, exercise: String, parameter: Double, url: String, weight: Int, set: Int, time: Int, hydro: String, minute: Int, part: String)
    }

    private var listener: OnItemClickListener? = null
    fun setOnItemClickListener(listener: OnItemClickListener){
        this.listener = listener
    }

    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view){
        var weight: String = "10"
        var time: String = "12"
        var set: String = "3"
        val profileImage: ImageView = view.findViewById(R.id.image)
        val levelImage: ImageView = view.findViewById(R.id.levelImage)
        val levelText: TextView = view.findViewById(R.id.part)
        val exerciseName: TextView = view.findViewById(R.id.exercise)
        val weightEdit: EditText = view.findViewById(R.id.weightEdit)
        val timeEdit: EditText = view.findViewById(R.id.timeEdit)
        val setEdit: EditText = view.findViewById(R.id.setEdit)
        private val saveCheckButton: AppCompatButton = view.findViewById(R.id.SaveButton)
        fun bind(model: TrainerDataModel.RequestingExerciseData, position: Int){

            exerciseName.text = model.exerciseName


            //운동 사진
            ExerciseModule().imageSetter(profileImage, model.exerciseUrl, true)

            weightEdit.hint = weight
            timeEdit.hint = time
            setEdit.hint = set

            //강도 이미지
            val level = ExerciseModule().getDoubleValue(model.exerciseParameter)
            ExerciseModule().levelFilter(level, levelImage, levelText)



            for(saved in savedList){
                if(saved.exerciseName == model.exerciseName){
                    weightEdit.setText(saved.data["Weight"].toString())
                    timeEdit.setText(saved.data["Time"].toString())
                    setEdit.setText(saved.data["Sets"].toString())
                    weightEdit.isEnabled = false
                    timeEdit.isEnabled = false
                    setEdit.isEnabled = false
                    saveCheckButton.isClickable = false
                    saveCheckButton.isPressed = true
                }
            }

            if(tempList.containsKey(position)){
                weightEdit.setText(tempList[position]!!.weight)
                timeEdit.setText(tempList[position]!!.time)
                setEdit.setText(tempList[position]!!.set)
                saveCheckButton.isPressed = tempList[position]!!.bool
            }


            saveCheckButton.setOnClickListener{
                val minute = ExerciseModule().minuteFilter(time, set)
                if(position != RecyclerView.NO_POSITION){
                    listener?.onItemClick(itemView,
                        position,
                        model.exerciseName,
                        model.exerciseParameter.toString().toDouble(),
                        model.exerciseUrl,
                        weightEdit.text.toString().toInt(),
                        setEdit.text.toString().toInt(),
                        timeEdit.text.toString().toInt(),
                        "AnAerobic",
                        minute.toInt(),
                        "Abs")
                    it.background = ContextCompat.getDrawable(it.context, R.drawable.ic_coaching_select)
                    it.isClickable = false
                    tempList[position] = TrainerDataModel.TempRequestingData(
                        weightEdit.text.toString(),
                        timeEdit.text.toString(),
                        setEdit.text.toString(),
                        null,
                        true
                    )
                }
            }
        }
    }
}

 

안드로이드는 기본적으로 상단에 ActionBar 제공한다.

 

Toolbar를 만들때 기본적인 방법은 

https://developer.android.com/training/appbar/setting-up?hl=ko&authuser=1 

 

앱 바 설정하기  |  Android 개발자  |  Android Developers

앱 바 설정하기 가장 기본적인 형태의 작업 모음은 한쪽에는 활동 제목을 표시하고 다른 쪽에는 더보기 메뉴를 표시합니다. 앱 바는 이렇게 간단한 형태로도 유용한 정보를 사용자에게 제공하

developer.android.com

상위 개발자 문서에서 이해할 수 있다.

예를 들어 다음과 같은 예를 보자.

 

단순 ConstraintLayout

이렇게 되었을 경우 우측 이미지 두개는 View 내 비즈니스 코드(onClickListener)로 처리해야 하며, 좌측 메뉴 버튼은 Toolbar의 기능을 가져오고 싶다.

 

이럴 경우 https://developer.android.com/reference/android/widget/PopupMenu

 

PopupMenu  |  Android Developers

android.net.wifi.hotspot2.omadm

developer.android.com

PopupMenu를 사용해준다.

A PopupMenu displays a Menu in a modal popup window anchored to a View. The popup will appear below the anchor view if there is room, or above it if there is not. If the IME is visible the popup will not overlap it until it is touched. Touching outside of the popup will dismiss it.[설명발췌]

-> 팝업 메뉴는 뷰에 고정된 뷰에 고정된 양식이 정해진 메뉴를 보여준다. 팝업은 만약 그 뷰에 공간이 있다면 그 아래로 보여지거나, 그 공간이 없다면 그 위로 보여진다. 만약 입력기(키패드와같은)가 보여질때 팝업은 그것이 터치 될때까지 오버랩 하지 않는다.

팝업 외의 부분을 터치할 경우 팝업은 사라진다.

 

 

이처럼 툴바에서 찾을 수 있는 기능들을 단순하게 팝업을 사용함으로써 간단하게 쓸 수 있다.

코드는 다음과 같다.

 

optionMenu = findViewById(R.id.memberTabMenuButton)
//memberTabMenuButton : ImageView

optionMenu.setOnClickListener {
    val popup = PopupMenu(this, it)
    val inflater: MenuInflater = popup.menuInflater
    inflater.inflate(R.menu.menu, popup.menu)
    //The int of R.menu.menu is Same as Toobar Layout Setting
    popup.setOnMenuItemClickListener {  menuItem ->
        when(menuItem.itemId){
            R.id.logout -> {
                Log.d("logout", "selected")
            }
        }
        true
    }
    popup.show()
}

 

DB: Firebase

Architecture: MVVM + LiveData

UI Flow

Calendar Item Click -> getData by matched Date -> refresh UI

 

GridSpacingItemDecoration Customizing

@SuppressLint("NotifyDataSetChanged")
fun trainerManager(recycler: RecyclerView, adapter: JournalFoodView.FoodJournalAdapter, foodData: MutableList<FirebaseFetchFood>, context: Context){
    Log.d("trainerManager","Called")
    adapter.dataList = foodData
    val gridLayoutManager = GridLayoutManager(context, 3)
    recycler.layoutManager = gridLayoutManager

    recycler.addItemDecoration(GridSpacingItemDecoration(3, 50, true))

    //Note: ItemDecorationCount
    //if(recycler.itemDecorationCount == 0){
    //    recycler.addItemDecoration(GridSpacingItemDecoration(3, 50, true))
    //}
    recycler.adapter = adapter
    adapter.notifyDataSetChanged()
}
class GridSpacingItemDecoration(private val spanCount: Int, private val spacing: Int, private val includeEdge: Boolean): RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildLayoutPosition(view)
//            .apply{ Log.d("position", "Called")}
        val column = position % spanCount
//        Log.d("getItemOffsets", "Called")
//        Log.d("column", column.toString())
        if(includeEdge){
            outRect.left = spacing - column * spacing / spanCount // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount // (column + 1) * ((1f / spanCount) * spacing)
            if(position < spanCount){
                outRect.top = spacing
            }
            outRect.bottom = spacing
        }else{
            outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing)
            outRect.right = spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f /    spanCount) * spacing)
//            Log.d("F outRect.left", "${column * spacing / spanCount}")
//            Log.d("F outRect.right", "${spacing - (column + 1) * spacing / spanCount}")
            if(position >= spanCount){
                outRect.top = spacing
            }
        }
    }
}

 

Condition 1.

Ex) 2022-06-13 날짜를 처음 클릭시 

Firebase AddValueEventListener -> send Data to UI

Condition 2.

EX) 2022-06-13 -> 2022-06-14 -> 2022-06-13 날짜 클릭 후 다른날짜 클릭후 다시 날짜를 클릭시

내부 과정

-> (2022-06-13 클릭)이미 리사이클러뷰 & 그리드 뷰는 CustomDecoration으로 한번 그려진 상태이다. 

-> (2022-06-14 ->  2022-06-13 재클릭) 이미 그려진 곳에 또 다시 그려지면서 그리드 뷰 아이템이 점점 내려간다.

 

원인

이미 그려진 상태에서 Decoration은 또 그려준다.. 이로 인해 점점 내려가는 것으로 확인

 

해결 방안

recyclerview의 Adapter의 데코레이션 카운트가 0인지 1인지 그 이상인지를 확인하면된다

 

if(recycler.itemDecorationCount == 0){
    recycler.addItemDecoration(GridSpacingItemDecoration(3, 50, true))
}

 

For 문 문법

for 문은 초기식 조건식 증감식으로 구성되어있다.

for문 흐름도

실습1

while 문을 for 문을 통해 구현해보자.

while문으로 만든 반복문

#include <stdio.h>

int main()
{
    int start,end,temp;
    int sum=0;
    scanf("%d %d",&start,&end);
    if(start>end){
        temp=end;
        end=start;
        start=temp;
    }
    for(int i=start;i<=end; i++)
    {
        sum+=i;
    }
    printf("sum= %d\n",sum);
    return 0;
}

 

실습2 Factorial을 구하는 함수

Factorial 흐름도

#include <stdio.h>
void exec_add(void){
    int start,end,temp;
    int sum=0;
    scanf("%d %d",&start,&end);
    if(start>end){
        temp=end;
        end=start;
        start=temp;
    }
    for(int i=start;i<=end; i++)
    {
        sum+=i;
    }
    printf("sum= %d\n",sum);
}
int factorial(int num){
    int facto=1;
    for(int i=num;i>=1;i--){
        facto*=i;
    }
    return facto;
}

int main()
{
    int num;
    int facto;
    scanf("%d",&num);
    facto=factorial(num);
    printf("%d",facto);
    return 0;
}

 

실습3. Combination 함수 만들기

#include <stdio.h>
void exec_add(void){
    int start,end,temp;
    int sum=0;
    scanf("%d %d",&start,&end);
    if(start>end){
        temp=end;
        end=start;
        start=temp;
    }
    for(int i=start;i<=end; i++)
    {
        sum+=i;
    }
    printf("sum= %d\n",sum);
}
int factorial(int num){
    int facto=1;
    for(int i=num;i>=1;i--){
        facto*=i;
    }
    return facto;
}
int combination(int n, int r){
    combi=factorial(n)/(factorial(n-r)*factorial(r));
    return combi;
}
int main()
{
    int n,r;
    int combi;
    scanf("%d %d",&n,&r);
    combi=combination(n,r);
    printf("%d",combi);
    return 0;
}

 

'Computer engineering > C' 카테고리의 다른 글

반복문-While(3)  (0) 2021.05.03
반복문-While(2)  (0) 2021.05.02
반복문-While(1)  (0) 2021.05.02
If-else 조건문  (0) 2021.04.21
If 조건문  (0) 2021.04.11

+ Recent posts