발견록_2

김재현·2023년 4월 24일
0

안드로이드

목록 보기
4/12

1. ViewModel에서 LiveData를 두가지로 나누기


다음과 같이 왜 뷰모델에서는 왜 MutableLiveData와 LiveData 두가지를 사용해야 할까?

  1. LiveData는 값이 변경될 때마다 등록된 옵저버에게만 변경 내역을 알려주는 단방향 데이터 흐름의 형태를 가진다. 즉, 옵저버는 LiveData의 데이터를 관찰만 할 뿐, 직접 수정할 수 없다.

  2. ViewModel의 속성을 LiveData 타입으로 선언하면, 이 속성을 관찰하는 옵저버는 ViewModel의 데이터를 변경 할 수 없으며, 오직 ViewModel 내부에서만 데이터를 수정할 수 있다. 이렇게 하면 액티비티나 프래그먼트에서 뷰모델의 데이터를 실수로 변경하는 일을 방지할 수 있다.

  3. MutableLiveData를 사용하면 ViewModel 내부에서 LiveData의 데이터를 변경할 수 있다. 이렇게 하면 LiveData가 값이 변경될 때마다 옵저버에게 알리고, 변경된 값을 옵저버에게 전달한다. 따라서 ViewModel 내부에서 데이터를 변경할 때는 MutableLiveData를 사용하고, ViewModel을 사용하는 액티비티나 프래그먼트에서는 LiveData타입의 속성으로 접근한다. 이렇게 하면 ViewModel과 뷰 간의 결합도가 낮아지고, 유지 보수성과 확장성이 좋아진다.


2. 문제가 발생했을때 ToastMessage 띄우기(MVVM)

ViewModel 내부의 함수에서 특정 상황이 되었을때 액티비티에 Toast메시지를 띄워야 하는 상황이다.

MutableLiveData의 타입을 String으로 받아서 띄워야 하는 메시지의 내용을 관찰하고 있으면 된다.
만약 조건에 걸렸다면 _showToastEvent.value에 String값을 넣어서 LiveData에 변경이 갔다는 점을 알려준다.
그후 액티비티에서 showToastEvent를 관측하는 옵저버를 호출해서 변경이 있으면 실행하게 하는데 지금은 데이터바인딩을 사용하였고 ViewModel의 생명주기를 Activity에 연결해놓은 상태이다. observe(this)로 들어오는 데이터는 우리가 적어준 String값이고 이를 람다식의 it으로 받아서 처리해준다.


3. TextView의 drawableStartCompat

글씨의 앞이나 뒤와 같이 글씨 주변에 이미지가 있어야 할때 사용할 수 있다.
위와 같은 이미지 처럼 말이다.
텍스트뷰에서 다음과 같이 그릴 이미지를 붙여주고 padding값을 주면 쉽게 그릴수가있다. 만약 중앙이 안맞는다면 gravity:center값을 주면된다.


4. 맨 상단으로 스크롤하게 해주는 버튼

만약 버튼을 만들어서 이게 어느 지점까지 스크롤이 되었을때 생기면서, 버튼을 누르면 제일 위로 올라가게 하고 싶을때 방법이다. ConstraintLayout으로 사용한다. 따로 FloatingButton을 사용하는게 아니고 그냥 ImageButton을 사용한다. ConstraintLayout특성상 맨 나중에 적는 코드는 맨 위에 오므로 가급적 맨 아래에 코드를 적는다.

end와 bottom을 parent에 붙여서 오른쪽 아래에 가게 만들고 마진으로 어느 정도 띄어둔다.
visibility:gone으로 처음 설정하여 맨 처음 로딩되었을때는 안보이게 설정하고 MainActivity에서 어느정도 스크롤이 되었을때 나타나게 한다.

액티비티에서 View.OnScrollChangeListener를 상속받는다.다음과 같이 상속받고 어느지점까지 오면 나타낼지 상수로 정해준다. 픽셀단위로 지정된다.(여기선 200px)

지금은 ViewBinding을 통해서 scrollView나 button을 가져와서 이벤트가 발생하면 특정상황을 실행한다. 만약 버튼을 클릭하면 scroll을 맨위로 올리게 한다.onCreate안에 만들어야한다.

액티비티에서 상속받은 OnScrollChangeListener에서 재정의 해야하는 onScrollChange를 재정의해준다.onCreate밖에 만든다.위와 같이 scrollY값이 특정부분을 넘어가면 버튼을 보였다 안보였다 하게 만들어준다. 이건 콜백함수로 실행되는것 같다.


5. 뷰페이저2로 이미지 무한 스크롤하기

뷰페이저2는 페이지 기반의 스크롤 방식으로 데이터를 보여준다. 하나의 페이지 안에 여러 뷰를 배치하고, 그 중 하나만 화면에 보여주는 방식.

필요한것
1. activity_main.xml에서 뷰페이저2가 위치할 자리
2. 어떤 생김새의 뷰가 들어갈것인지 보여줄 새로운 .xml파일
3. Adapter를 정의한 파일
4. Adapter안의 뷰홀더
5. MainActivity에서 뷰페이저2가 위치할 자리와 Adapter파일간의 연결

  1. 다음과 같이 activity_main.xml에 뷰페이저2로 이미지를 슬라이드 할 수 있는 공간을 만들어준다.
  1. 새로운 xml파일을 만들어서 이미지는 어떻게 넣을건지 디자인한 파일을 만들어둔다.여기선 화면 전체를 ImageView로 잡았는데 그 이유는 scaleType을 통해 이미지를 알아서 잘라서 크기 조절을 해주게 하기 위해서이다.
  1. Adapter를 정의한 class 파일, 4. Adapter안의 뷰홀더 class

    어뎁터 파일은 MainActivity에서 연결할것을 상정하고 만든다. 그래서 지금은 class의 매개변수로 이미지의 주소가 담긴 ArrayList를 받아온다. override하는 함수들은 RecyclerView.Adapter를 상속하면 자동완성으로 받아진다. onCreateViewHolder에서는 inflate할 레이아웃으로 2번에서 만든 새로운 레이아웃을 넣어준다.

주황색 밑줄을 적어놓은 곳이 무한 스크롤하기 위한 장치이다. 무한 스크롤의 동작원리로 미리 무수히 많은 이미지를 만들어놓은 다음에 사용하는 그 중간부터 보게 하는 것이다.
onBindViewHolder에서는 4번에서 정의한 ViewHolder의 프로퍼티와 함수들을 사용할 수 있다.

  1. MainActivity에서 뷰페이저2가 위치할 자리와 Adapter파일간의 연결onCreate()의 내부에 viewbinding을 사용하여 어뎁터를 연결한 코드이다. 1번에서 정의한 뷰페이저2의 아이디를 가져오기만 하면 되므로 findViewBy...로 val부분을 만들어도 상관없다.
    그 후 viewPager.adapter로 만들었던 3,4번에서 만들었던 adapter를 연결해주고 로딩후 현재위치인 뷰페이저2의 첫번째 위치를 위와같이 설정하여 마치 맨 첫번째를 보는 것처럼 설정해준다.(사실은 중간위치)

만약 Adapter의 getItemCount()를 개수만큼만 만들었다면 오른쪽끝, 왼쪽끝에서 더이상 스크롤이 되지 않았고, 충분한 수를 만들었어도 6번에서 인디케이터와 연결하면 오른쪽 끝으로는 무한스크롤이 되는데 왼쪽끝에서는 스크롤이 되지 않는 이상한 현상이 나타났다.


6. 뷰페이저2로 무한 스크롤한 곳에 인디케이터 만들기

무한 스크롤로 뷰페이저2를 사용하여 만든것은 좋은데, 내가 몇번째 이미지를 보고있는지 알 수 있어야 사용자 친화적일 것이다.

만약 무한 스크롤을 하지 않는다면 Material View Pager Dots Indicator처럼 오픈소스를 활용하여 예쁜 인디케이터를 만들 수 있을지도 모른다.

무한 스크롤에 인디케이터를 붙이기 위해 했던 노력과 왜 안됐는지 적어보고자 한다.

  1. 오픈 소스를 활용하기
    처음에는 위와 같은 오픈소스를 활용하였다. 그런데 위의 5번에서 설명했던 곳에서 처럼 Adapter안에는 내가 몇개의 아이템을 만들것인지 적어줘야 하는 부분이 있다. 이 부분을 통해 오픈 소스가 indicator를 만드는데 Int형의 최대 자리수를 return하게 되면 진짜 무수히 많은 수의 indicator가 만들어져 문제가 생겼다. MainActivity에서 제어를 해줘야 할거 같은데 방법을 몰라서 이 방법은 제외했다.
  1. TabLayout 사용하기
    TabLayout을 사용했을때도 위와 같이 무수히 많은 수의 indicator가 만들어졌다. TabLayout은 drawable에 선택 되었을때와 선택 안되었을때, selector 총 3개의 xml파일을 만들었어야 했다. 그런데 TabLayout과 뷰페이저2를 연결하기 위해 TabLayoutMediator(tabLayout, viewPager2)를 사용하는 순간 자동으로 getItemCounnt의 무수히 많은 indicator를 만들어서 소용이 없었다.
    getItemCount()의 개수를 무수히 많이 해야하는데 그렇지 않으면 말짱도루묵이므로 Mediator를 사용하지 않는 방법을 찾아보았다.
    을 사용하여 MainActivity에서 urls리스트 크기만큼 tabLayout의 addTab을 해주려고 했다. 이렇게 한다고 만들어 질 수는 있지만 뷰페이저2와 연결하면서 페이지가 바뀌었을때 indicator가 바뀌게 할 수는 없었다. 결국 연결은 TabLayoutMediator를 사용하는데 그 순간 여러개의 indicator가 생겨버리기 때문이였다. 이 문제를 해결하기 위해 여러 리스너나 removeAllTabs같은 함수를 사용해보았지만 해결되지 않았다.

해결책

리사이클러뷰를 사용하여 indicator를 만들고 뷰페이저2와 연결하기

리사이클러뷰를 사용하여 horizontal모양의 view를 만들어서 사용한다.
리사이클뷰를 위해 indicator모양을 만든 xml으로 marginHorizontal을 통해 각 뷰가 떨어져있게 만들어서 마치 indicator처럼 보이게 만드는 방법이다.

이 방법으로 주의해야할 점은 총 몇개의 이미지가 있는지와 현재 몇번째 위치의 이미지를 보고있는지를 Adapter에 넘겨주어야한다는 점이다.

Adapter파일Adapter의 매개변수로 이미지 리스트의 사이즈를 받아와서 그 수 만큼 리사이클뷰를 만든다.(getItemCount()부분) 그리고 MainActivity에서 정의해줄 현재 이미지의 위치를 가져오는 setCurrentPosition함수를 통해 프로퍼티인 currentPosition을 갱신해준다. 그렇게 가져온 정보를 토대로 ViewHolder에서 현재 보고있는 위치와 position이 일치하면 리사이클러뷰의 background이미지를 미리 그려놓은 dot이미지로 선택할 수 있도록 만든다.

MainActivity에서 리사이클러뷰와 뷰페이저2를 연결해주기activity_main에 만들어놓은 RecycleView의 adapter에 방금 만들었던 adapter를 연결해준다. layoutManager를 통해서 가로로 자동으로 만들어지도록 해준다.
그후, viewPager가 변경될때 리스너를 추가해줘서 현재 페이지가 리스트의 몇번째인지 전달해줄 수 있도록 콜백함수를 만들어준다.

이렇게 하면 리스트의 개수만큼 indicator가 만들어질 뿐만 아니라 현재 페이지에 맞춰서 indicator가 역할을 할 수 있어진다.이렇게 만들어진다는 소리이다.


7. Glide 사용방법

글라이드 사용법
5,6번을 통해 충분히 많은 숫자의 이미지를 만드는데 그냥 이미지를 불러온다면 런타임 오류가 난다거나 메모리에 계속 적재해두는 문제를 통해 성능 저하의 문제가 나타났었다. 해결책으로 글라이드를 통한 이미지 처리를 하는 방법이 많아서 glide를 사용했었다.

만약 다운받아놓은 이미지가 아니라 url을 통해서 이미지를 넣고 싶다면 manifest에 로 internet을 추가해주어야 한다.


8. 상태 표시줄 없애기

Android status bar 투명하게 만들기의 블로그 내용을 참조하였다.
res>values>themes>themes.xml에서 몇개의 설정을 통해 액션바를 없애고, 상태표시줄을 반투명하게 만들 수 있다.NoActionBar를 통해 액션바를 없애고 windowTranslucentStatus를 통해서 반투명하게 만들어준다.

// 적용
binding.clMainLayout.setPadding(0,getStatusBarHeight(requireContext()), 0, 0)


// extention.kt
fun getStatusBarHeight(context: Context): Int {
    val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")

    return if (resourceId > 0) {
        context.resources.getDimensionPixelSize(resourceId)
    } else {
        0
    }
}

getStatusBarHeight를 통해서 얻어온 상태 표시줄의 높이를 다른 곳들의 마진이나 높이로 추가해서 레이아웃을 맞춰줄 수 있다.


9. 커스텀 다이얼로그 만들기

버튼을 누르면 Block과 Report를 할 수 있는 창을 띄우고자 한다. Gone으로 나뒀다가 버튼을 클릭했을때 Visible로 보이게 하는 방법이 있지만 지금은 커스텀 다이얼로그를 사용해보자.

  1. 다이얼로그로 보여줄 xml을 만든다.다음과 같이 생긴 xml파일이다. constraint안에 text,view,text가 있는 형태로 constraintlayout의 background로는 drawable에 그려둔 테두리가 좀 깍여있는 다이얼로그를 가져오려고했다.

  2. 이후 다이얼로그를 상속받는 클래스를 만들고 그 클래스는 context를 매개변수로 받는다.
    이렇게 하면 onCreate내부에서 버튼이 눌렸을때의 동작과 다이얼로그의 위치 등을 조절할 수 있다.

  3. 2번에서 만든 다이얼로그를 mainactivity에서 연결하고 show해준다.나는 버튼이 눌렸을때 다음과 같이 동작하면 좋겠다고 정의했다.
    다이얼로그 클래스에 context를 넣어준다.
    builder.window!!.setDimAmount(0f)는 다이얼로그를 클릭했을때 주변 화면이 반투명 검은색으로 나오는걸 다시 하양게 만들어주는 걸 뜻한다.
    builder.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))는 다이얼로그를 그렸어도 그 크기가 맞지 않아 다이얼로그의 기본 배경색이 삐져나오는것을 뜻한다.
    builder.show()를 하면 화면에 그려지게 된다.

    원래는 dialog클래스를 만들지 않고 AlertDialog.Builder(this).run{...}을 사용했었다. 그렇게 하면 내가 원하는 xml과 맞지 않는 상황이 나왔고 위치를 주기 힘들었었다. 지금은 이렇게 테두리가 있는 것으로 나오지만 사실은 .xml에 적은 내용이 적용된게 아닐 지도 모른다. Dialog자체에서 테두리를 잘라서 둥그렇게 만들어주기 때문에 그것이 적용되었을지도 모르겠다.


10. ConstraintLayout의 y좌표를 알아야 할때

특정 constraintlayout의 위치를 넘어서 스크롤을 했을때 무언가가 동작해야할때 constraintlayout의 y좌표를 알아야 한다. 이때 사용한 방법이다.

constraintlayout은 onCreate()에서 다 로딩 되지 않아서 onCreate에서 구하고자 해도 0으로 나온다. onCreate()에서 스크롤 리스너 같이 계속 보고있는 다면 상관없지만 스크롤 할 때마다 계속 부르기 때문에 뭔가 낭비같이 느껴졌었다.

onWindowFocuschanged() 메소드를 사용하여 액티비티의 창이 포커스를 받았거나 잃었을때 y좌표를 계산하게 했다.onWindowFocusChanged()는 액티비티의 생명주기 콜백 함수로 해당 액티비티가 사용자의 화면에서 활성화되어 사용자의 입력을 받을 준비가 되었을때를 뜻하기 때문에 리스너로 여러번 듣는것보다는 괜찮을 것이라고 생각했다.

그래서 스크롤이 되서 특정 위치에 간다면 처럼 특정 이벤트가 일어나게 만들어주었다.

2023-04-26(수) 추가
onWindowFocusChanged()메소드를 활용해서 특정값을 찾았다면 이것은 onCreate안에서는 사용할 수 없다. 위처럼 함수로 빼서 이벤트가 일어났을때 사용한다면 몰라도... 그 때를 위해서는 viewTreeObserver를 사용해서 화면이 로딩되는것을 지켜본다. TreeObserver는 계~~~속 실행되므로 리스너 삭제를 꼭 해주어야 한다. 이것을 통해 onCreate()단에서도 특정 레이아웃의 높이,너비,x,y값등을 알 수 있다.


profile
배운거 정리하기

0개의 댓글