최종 결과 이미지

 

평범한 BottomNavigationView 에 질리셨다면, 이제는 조금 변경해볼까요?

지금부터 순서대로 따라하시면 위와 같은 결과를 얻을 수 있습니다! (6단계의 과정만 진행하시면 됩니다!)

 

 

1. Theme 수정하기
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.YoutubeLayout" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">#0F9D58</item>
        <item name="colorPrimaryVariant">#0F9D58</item>
        <item name="colorOnPrimary">#000000</item>
    </style>
</resources>
2. Menifest 수정하기
<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/Theme.YoutubeLayout">
  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>
3. menu Item 추가하기

1. 사전에 Vector Icon 을 만들어주세요 ^_^

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/home"
        android:icon="@drawable/ic_home"
        android:title="Home"/>

    <item
        android:id="@+id/Search"
        android:icon="@drawable/ic_search"
        android:title="Search"/>

    <item
        android:id="@+id/placeholder"
        android:title=""/>

    <item
        android:id="@+id/Profile"
        android:icon="@drawable/ic_favorite"
        android:title="Favorite"/>

    <item
        android:id="@+id/Settings"
        android:icon="@drawable/ic_locker"
        android:title="Locker"/>

</menu>
4. Custom BottomNavigationView 생성하기

이렇게 해주지 않으면, 공백에 해당하는 Item 에도 클릭이벤트가 발생하기 때문에, 간단하게 생성해주세요 :)

class YoutubeBottomNavigationView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr) {

    init {
        val menuView = getChildAt(0) as ViewGroup
        //index 2 : 비활성화 아이템
        menuView.getChildAt(2).isClickable = false
    }
}
5. MainActivity.xml 수정하기

app:elevation="0dp" 가 왜 필요하죠?
* android:elevation="0dp" 가 아니라, app:elevation="0dp" 라는 점을 주의하세요!

BottomNaviagtionView 에는 기본적으로 app:elevation 이 8dp 로 적용되어있습니다. 따라서, 0dp 로 해주지 않으면 그림자 잔상효과가 남아요!

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom">

        <com.devhyeon.youtubelayout.YoutubeBottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginEnd="16dp"
            app:elevation="0dp"
            app:labelVisibilityMode="labeled"
            android:background="@android:color/transparent"
            app:menu="@menu/bottom_nav_menu" />

    </com.google.android.material.bottomappbar.BottomAppBar>


    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/app_name"
        android:src="@drawable/ic_add"
        app:layout_anchor="@id/bottomAppBar" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

6. 결과 확인하기

 

다양하게 NavagationView 를 사용해보면 좋을 것 같아요! :)

github 프로젝트를 본 학생분께서 질문메일을 보내주셨습니다. (관심있게 봐주셔서 감사합니다.)

저 메일을 받고, 머리를 탁 맞은 기분이였습니다.

단순예제를 보이기 위함이였지만, 그럼에도 설명이 많이 부족했구나를 깨달았습니다.

그에따라 해당 부분에 대한 코드를 수정하였는데요.

 

nonce 를 생성하는 더 좋은 코드와 방법이 있다면, 답글로 알려주시면 감사하겠습니다 (꾸벅)

 

/**
 * Nonce : 안드로이드에서 SafetyNet 에서 사용
 * SafetyNet Attestation API를 호출할 때 nonce를 전달해야 합니다.
 * SafetyNet 요청에 사용되는 nonce는 길이가 16바이트 이상이어야 합니다.
 * */
class SafetyUtils {
    //랜덤한 문자열을 생성하기 위한 BASE String
    private fun loadBaseString(): String {
        return "abcdefghijklmnopqrstuvwxyz0123456789_-"
    }

    //현재 년월일시분초밀리초를 반환
    private fun createDateNow(): String {
        val current = LocalDateTime.now()
        val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")
        return current.format(formatter)
    }

    //결합하여 고유하면서 랜덤한 nonce 생성 (Size : 34)
    fun createNonce() : ByteArray {
        val dateStr = createDateNow()
        val baseStr = loadBaseString()
        val nonce = StringBuilder()

        for (i in dateStr.toCharArray()) {
            //date 로 BASE 참조 (고유) : 0~9 를 참조하기 위해 i - ASCII
            nonce.append(baseStr[i.toInt()-48])
            //랜덤하게 BASE 참조 (랜덤)
            nonce.append(baseStr[Random().nextInt(baseStr.length - 1)])
        }

        return nonce.toString().toByteArray()
    }
}
val FIREBASE_AUTH_NONCE = SafetyUtils().createNonce()

 

앞으로 github 에 코드를 작성할 때, 코드를 보는 사람이 추가적으로 작성을 해야하는 부분이나, 이미 작성된 부분에 대해서

보다 정확한 설명이 필요하겠구나를 느끼게 되었네요..

 

코드기반 질문을 해주셔서 다시 한번 감사합니다.

권한이 없는 것도 확인이 되고, 권한 요청을 시도해야하는 Cusom UI 까지 동작을 합니다. (권한이 없기때문에)그런데 권한요청팝업을 띄우는 코드가 동작하지 않습니다. 

 

라는 질문을 받았습니다.

제 처음 답변은 

"정말 코드가 정상적으로 동작하는 코드라는 확신이 있거나, 이전에 동작하는것에 문제가 없다면"

안드로이드 버전에 따른 이슈 등을 확인하여 변경된 사항을 체크해야 한다는 것을 알려주었습니다.

 

그러나, 쉽사리 해결하지 못하고 있었기에 직접 찾아보았습니다.

 

1. 앱에서 위치데이터를 수집하기 위해 아래와 같은 퍼미션을 추가하였다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

 

2. 그러나 질문자는 이전처럼 위치권한을 수락해도, 백그라운드에서 데이터를 수집할 수 없다는 것을 알고 있다.

따라서, 아래와 같은 백그라운드에서도 수집할 수 있도록 퍼미션을 추가하였다.

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

 

3. 그 후, 권한 요청을 시도하였지만, 팝업창이 뜨질 않았다.

그로인해 질문을 하게 되었다..

 

왜 권한요청 팝업이 뜨지 않았을까?

 

공식문서에 의하면,

1. 위치권한을 요청한다.

2. 백그라운드 위치권한이 필요한 경우에는 필요한 순간에 추가적으로 요청한다.

를 권장하고 있습니다.

 

물론, 질문자도 여기까지는 이해하고 있었지만, "권장" 이라는 단어에 의하여 실 적용이 강제되지는 않는다고 생각하고 넘겼습니다.

 

하지만, 공식문서를 천천히 읽어본다면,

 

1.  API29 부터는 백그라운드 위치 권한이 있어야만 모든 상황에서 현재 위치에 대한 데이터를 얻을 수 있다.

2. 그러나, 일부 기능에서만 백그라운드 위치 정보 권한이 필요할 수 있기 때문에, 포그라운드 위치 정보를 요청하고, 이후에 필요에 따라 백그라운드 위치정보 권한을 얻는 방식을 권장한다.

3. 그러나!! API30 부터는 권장사항을 완전히 적용하였기 때문에, 포그라운드의 위치권한과 백그라운드의 위치권한을 동시에 요청한다면, 어떠한 동작도 하지 않는다고 안내하고 있다.

 

4. 따라서 권한요청 팝업을 띄울때, 두 경우를 동시에 체크하고, 동시에 요청하는 방식으로 코드를 작성한다면, API 30부터는 동작하지 않는다.

- 권장사항대로 작성하는 것이 가장 좋겠지만, API30 미만 (API29까지) 처럼 한 화면에서 모든 권한을 다 체크하고 진행하고 싶다면, 

A. 백그라운드 위치권한을 제외한 나머지 권한을 체크한다. (포그라운드 위치권한 포함)

B. 모든 권한이 허용된다면, 백그라운드 위치권한을 체크하고 요청한다.

의 순서로 진행하면 될 것 같다.

 

만약,

requestPermissions(permissionRequestArray, REQUEST_CODE ...

처럼 동시에 없는 권한을 요청하는 코드로 API30 이후의 기기까지 호환시키려는 경우라면, 4번처럼 하는 것이 의도대로 동작할 수 있을 것 같다.

https://developer.android.com/topic/libraries/architecture/datastore

 

SharedPreferences

SharedPreferences 는 키-값 의 데이터를 저장하기 위해 사용합니다. 이때, 저장은 File 에 이루어집니다.

File 에 저장을 하고 읽는 다는 것은, 데이터를 유지할 수 있다는 것을 의미합니다.

 

저는 "앱 사용자설정" 등의 설정값들을 저장하기 위해 많이 사용합니다.


DataStore

DataStore 도 키-값 의 데이터를 저장하기 위해 사용합니다. 역시나 저장은 File 에 이루어집니다.

 

그렇다면 왜 DataStore 를 사용하는 것이 더 좋을까요?

(반대의견이 있을 수도 있다고... 생각...합니다만.. 일단 작성해보겠습니다.)

 

1. DataStore 는 코틀린 코루틴을 사용한 Flow 를 사용하여 비동기적으로 저장됩니다.

(SharedPreferences 는 읽는 용도로 사용될때 비동기를 지원하였습니다.)

 

2. DataStore 는 내부적으로 DisPatchers.IO 로 동작하고 있기 때문에 MainThread 로부터 안전합니다.

 

위 두 내용만 본다면, 음... 그다지.. 그냥 기존에 쓰던걸 쓰면 안되나? 그게 더 편할듯.. 이라고 생각하실 수 있습니다.


Preferences DataStore vs Proto DataStore

 

Preferences DataStore

- 위에 말한 두가지의 장점을 포함하여 SharedPreferences 보다 몇가지 개선된 점을 갖고 있는 DataStore 입니다.

 

Proto DataStore 

- 이것이 진짜입니다.

1. Protocol buffers 를 이용하여 객체를 저장할 수 있다.

(Preferences 는 객체를 저장할 수 없기 때문에 직렬화와 역직렬화를 위해 번거로운 작업이 있었습니다.)

 

2. 타입안전성을 제공합니다. 

Preferences DataStore 역시 타입에 대한 안전성을 제공하지는 않았습니다. 그러나, Proto DataStore 는, 1번에 작성 된 것처럼

Protocol buffers 를 사용하기 때문에 타입에 대한 안전성을 제공합니다.

 

3. Key 를 사용할 필요가 없습니다.

Proto DataStore 는 지정되어있는 타입을 인식하고 제공하기 때문에 Key 를 사용할 필요가 없습니다.


SharedPreferences VS PreferencesDataStore vs ProtoDataStore

 

https://developer.android.com/codelabs/android-preferences-datastore#3

 

 

 

참고 https://developer.android.com/topic/libraries/architecture/datastore

 

Datastore  |  Android 개발자  |  Android Developers

Datastore   Android Jetpack의 구성요소. Jetpack Datastore는 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다. Datastore는 Kotlin 코루틴 및 Flow를

developer.android.com

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android CodeLab [Compose 개요]  (2) 2022.11.12
Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19

서비스(Service)

서비스(Service)는 사용자와 상호 작용하지 않고 더 오래 실행되는 작업을 수행하려는 응용 프로그램 구성 요소 또는 다른 응용 프로그램이 사용할 수 있는 기능을 제공하는 응용 프로그램 구성 요소이다.

사용자와 상호 작용하지 않고 : UI 가 존재하지 않습니다.
더 오래 실행되는 작업을 수행 : 백그라운드에서 작업을 수행합니다.
다른 응용 프로그램이 사용할 수 있는 : A 앱에서 B 앱의 기능을 사용할 수 있습니다. (B앱의 서비스가 이를 제공해야함)

* 서비스는 별도의 프로세스가 아니다.
- 서비스를 백그라운드에서 돌릴 수 있지만, 서비스 자체가 별도의 프로세스는 아닙니다. 따로 지정하지 않는 이상, 서비스는 앱의 프로세스와 동일한 프로세스에서 동작합니다.

* 서비스는 쓰레드가 아니다.
- 흔히 앱에서 백그라운드쓰레드로 작업을 처리하는 경우가 있는데,
서비스는 단지, 백그라운드에게 "수행해야 할 작업" 이 있다 를 "알리는" 것일 뿐입니다.

* 서비스는 프로세스 내의 주 쓰레드에서 동작하기 때문에, MP3 재생 등과 같은 기능을 하려면 별도의 쓰레드를 서비스 내에 만들어 주어야 한다.

서비스 LifeCycle

  • onStartCommand() 시스템이 이 메서드를 호출하는 것은 또 다른 구성 요소(예: 액티비티)가 서비스를 시작하도록 요청하는 경우입니다. 이때 startService()를 호출하는 방법을 씁니다. 이 메서드가 실행되면 서비스가 시작되고 백그라운드에서 무한히 실행될 수 있습니다. 이것을 구현하면 서비스의 작업이 완료되었을 때 해당 서비스를 중단하는 것은 개발자 본인의 책임이며, 이때 stopSelf() 또는 stopService()를 호출하면 됩니다. 바인딩만 제공하고자 하는 경우, 이 메서드를 구현하지 않아도 됩니다.
  • onBind() 시스템은 다른 구성 요소가 해당 서비스에 바인딩되고자 하는 경우(예를 들어 RPC를 수행하기 위해)에도 이 메서드를 호출합니다. 이때 bindService()를 호출하는 방법을 사용합니다. 이 메서드를 구현할 때에는 클라이언트가 서비스와 통신을 주고받기 위해 사용할 인터페이스를 제공해야 합니다. 이때 IBinder를 반환하면 됩니다. 이 메서드는 항상 구현해야 하지만, 바인딩을 허용하지 않으려면 null을 반환해야 합니다.
  • onCreate() 시스템은 서비스가 처음 생성되었을 때(즉 서비스가 onStartCommand() 또는 onBind()를 호출하기 전에) 이 메서드를 호출하여 일회성 설정 절차를 수행합니다. 서비스가 이미 실행 중인 경우, 이 메서드는 호출되지 않습니다.
  • onDestroy() 시스템이 이 메서드를 호출하는 것은 서비스를 더 이상 사용하지 않고 소멸시킬 때입니다. 서비스는 스레드, 등록된 리스너 또는 수신기 등의 각종 리소스를 정리하기 위해 이것을 구현해야 합니다. 이는 서비스가 수신하는 마지막 호출입니다.

Context  |  Android 개발자  |  Android Developers

developer.android.com

백준에서 코틀린으로 알고리즘 문제를 풀던 중에, 테스트코드를 직접 입력하고, 출력이 맞는지 눈으로 확인하는 과정이 너무 힘들었다.

 

그래서 테스트 코드를 사용해보고자 하였다.

 

Android 에서 Junit 으로 잠시 사용해본 적은 있기에 어려울 것 같지는 않았으며, 실제 사용해보니 동일? 했다.

 

 

먼저, 테스트를 하고자하는 클래스를 우클릭하면 Test 코드를 쉽게 생성할 수 있다.

 

 

 

그럼 이렇게, 해당 클래스이름 뒤에 Test 가 붙으면서 internal class 가 하나 생성된다.

테스트하고자 메소드는 생성할 때, 체크하는 부분이 있다.

선택하게 되면, 구현체는 없는 메소드가 생성되어있다.

 

그 이후에 테스트하고자 하는 클래스를 생성하고, assertEquals 를 사용하여, 입력에 따른 출력이 내가 생각한 결과와 같는지 비교하면 된다.

 

만약 flase 라면, ERROR 가 발생한다 :)

 

이제 테스트코드를 하나씩 입력하고, 주석으로 가리고 제출하는 등의 번거로운 작업을 피할 수 있게 되었다 홓...

1번 문제 

생각보다 쉬웠다.

5~10분정도 걸린 것 같다.

 

 

2번 문제

분명, 어디서 많이 봤는데.. 이 문제는 시간복잡도를 고민해야하는데...

2가지 고민을 했다.

1. 시간복잡도를 고려하여 문제를 풀어낸다

2. 일단 정확성으로 풀고, 이후에 다시 풀어낸다.

 

나는 2번을 생각했고, 정확성만 처리하고 넘겼다. 총 고민에서 넘김까지 15~20분 정도 걸린것 같다.

 

3번 문제

시간을 적지않게 소모하는 구현문제라고 봐야할 것 같다.

1시간 가량을 이 문제 구현에 소모했다..

 

그러고나니, 30분안에 2번문제의 시간복잡도를 고려하여 다시 풀어내야하는데.....

결국 만족스러운 결과를 내지 못했다.. (속상하네..)

 

결과는 2솔 + 2번 부분점수

일 것으로 생각된다....

 

어짜피 Android 개발밖에 안해봐서.. 면접에 간다해도 iOS 직군이니 떨어지겠지...?

어떻게 공부는 하면할수록, 알면알수록, 내가 모르는게 너무 많다는걸 느끼면서 자신감이 떨어지는 것 같기도....

 

+ Recent posts