<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>k_hyun.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 19 Jun 2023 06:06:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. k_hyun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/k_hyun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Compose의 상태 (2)]]></title>
            <link>https://velog.io/@k_hyun/Compose%EC%9D%98-%EC%83%81%ED%83%9C-2</link>
            <guid>https://velog.io/@k_hyun/Compose%EC%9D%98-%EC%83%81%ED%83%9C-2</guid>
            <pubDate>Mon, 19 Jun 2023 06:06:01 GMT</pubDate>
            <description><![CDATA[<h2 id="상태-호이스팅">상태 호이스팅</h2>
<p>내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기가 더 어려운 경향이 있다.</p>
<p>상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴</p>
<p>컴포저블이 가능한 한 적게 상태를 소유하고 적절한 경우 컴포저블의 API에 상태를 노출하여 상태를 끌어올릴 수 있도록 컴포저블을 디자인해야 한다.</p>
<h3 id="counter-분리">Counter 분리</h3>
<pre><code class="language-kotlin">@Composable
fun StatefulCounter(modifier: Modifier) {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

@Composable
fun StatelessCounter(count: Int, onIncrement: () -&gt; Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count &gt; 0) {
           Text(&quot;You&#39;ve had $count glasses.&quot;)
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count &lt; 10) {
           Text(&quot;Add one&quot;)
       }
   }
}</code></pre>
<p>기존에 상태를 가진 Counter를 Stateful, Statelesss 컴포저블로 분리할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/93fb1d18-8d1f-40e3-8fc0-576773ac6e2d/image.png" alt="">
사용자가 juiceCount의 값을 변경하면 StatefulCounter가 리컴포즈 된다.</p>
<p>그리고 juiceCount를 읽는 StatelessCounter(juiceCount)만 리컴포즈 된다.
하지만 StatelessCounter(waterCount)는 리컴포즈 되지 않음</p>
<h2 id="관찰-가능한-mutablelist">관찰 가능한 MutableList</h2>
<p>목록에서 작업을 삭제하는 동작을 추가하려면 먼저 목록을 변경 가능한 목록으로 만들어야 한다.</p>
<p>이를 위해 변경 가능한 객체(예: ArrayList<T> 또는 mutableListOf,)를 사용하면 작동하지 않음  </p>
<h3 id="주의-사항">주의 사항</h3>
<pre><code class="language-kotlin">  //경고: 대신 mutableStateListOf API를 사용하여 목록을 만들 수 있습니다.
  //그러나 이를 사용하는 방식으로 인해 예기치 않은 리컴포지션이 발생하고 UI 성능이 최적화되지 않을 수 있습니다.

//목록을 정의하고 작업을 다른 작업에 추가하면 모든 리컴포지션에 중복된 항목이 추가됩니다.

// Don&#39;t do this!

val list = remember { mutableStateListOf&lt;WellnessTask&gt;() }

list.addAll(getWellnessTasks())

대신 단일 작업으로 초깃값을 사용하여 목록을 만든 후 다음과 같이 remember 함수에 전달합니다.

// Do this instead. Don&#39;t need to copy

val list = remember {

mutableStateListOf&lt;WellnessTask&gt;().apply { addAll(getWellnessTasks()) }

}</code></pre>
<h3 id="작동-순서">작동 순서</h3>
<p>  <img src="https://velog.velcdn.com/images/k_hyun/post/73e3d05a-c2ae-41e3-aafb-5fddb39bd71d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose의 상태 (1)]]></title>
            <link>https://velog.io/@k_hyun/Compose%EC%9D%98-%EC%83%81%ED%83%9C-1</link>
            <guid>https://velog.io/@k_hyun/Compose%EC%9D%98-%EC%83%81%ED%83%9C-1</guid>
            <pubDate>Thu, 15 Jun 2023 13:41:11 GMT</pubDate>
            <description><![CDATA[<h2 id="event">Event</h2>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/5bd767ea-d2eb-460f-a75c-d3095f1bc3ad/image.png" alt="">
이벤트 </p>
<pre><code>-    이벤트는 사용자 또는 프로그램의 다른 부분에 의해 생성된다.</code></pre><p>상태 업데이트</p>
<pre><code>-    이벤트 핸들러가 UI에서 사용하는 상태를 변경한다.</code></pre><p>상태 표시</p>
<pre><code>-    새로운 상태를 표시하도록 UI가 업데이트한다.</code></pre><h2 id="composition">Composition</h2>
<blockquote>
<p>컴포저블을 실행할 때 Compose에서 빌드한 UI에 관한 설명을 컴포지션이라고 한다.</p>
</blockquote>
<p>컴포지션</p>
<pre><code>-    컴포저블을 실행할 때 Jetpack Compose에서 빌드한 UI에 관한 설명.</code></pre><p>초기 컴포지션</p>
<pre><code>-    처음 컴포저블을 실행하여 컴포지션을 만든다.</code></pre><p>리컴포지션</p>
<pre><code>-    데이터가 변경될 때 컴포지션을 업데이트하기 위해 컴포저블을 다시 실행하는 것.</code></pre><h2 id="state">State</h2>
<h3 id="remember--mutablestateof">remember &amp; mutableStateOf</h3>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/823075be-5d2f-4489-ae47-1b1cf038c8c1/image.gif" alt=""></p>
<pre><code class="language-kotlin">@Composable
fun WaterCounter(modifier: Modifier) {

    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }

        Text(text = &quot;You&#39;ve had $count glasses.&quot;, modifier = modifier.padding(16.dp))
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
            Text(text = &quot;Add one&quot;)
        }
    }
}</code></pre>
<p>remember 과 mutableStateOf를 사용해 변수의 값을 메모리에 보관할 수 있다.</p>
<p>리컴포지션이 되어도 그 값이 유지가 된다.</p>
<p>반면 리컴포지션이 호출 되지 않는다면 해당 부분의 remember 변수는 삭제가 되는데 아래의 사례가 있다.</p>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/83a03a03-8b10-4488-8bb2-a193202626a3/image.gif" alt=""></p>
<pre><code class="language-kotlin">@Composable
fun WaterCounter(modifier: Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }

        if (count &gt; 0) {
            var showTask by remember { mutableStateOf(true) }
            if (showTask) {
                WellnessTaskItem(
                    modifier = modifier,
                    onClose = { showTask = false },
                    taskName = &quot;Have you taken your 15 minute walk today?&quot;
                )
            }
            Text(&quot;You&#39;ve had $count glasses.&quot;)
        }

        Row(Modifier.padding(top = 8.dp)) {
            Button(onClick = { count++ }, enabled = count &lt; 10) {
                Text(&quot;Add one&quot;)
            }
            Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
                Text(&quot;Clear water count&quot;)
            }
        }
    }
}</code></pre>
<p>count를 0으로 초기화 하면 if (count &gt; 0) {} 안의 내용들이 리컴포즈 되지 않는다.</p>
<p>따라서 showTask의 값은 삭제가 되고, 나중에 다시 그려질 수 있다.</p>
<h3 id="remembersavable">rememberSavable</h3>
<p>위의 코드들은 화면 전환이 발생했을 때 count의 값이 초기화 된다.</p>
<p>remember 대신 rememberSavable을 사용하면 Bundle에 저장할 수 있는 모든 값을 자동으로 저장하고, 따라서 데이터가 유지되는 것을 확인할 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 기본 레이아웃]]></title>
            <link>https://velog.io/@k_hyun/Compose-%EA%B8%B0%EB%B3%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83</link>
            <guid>https://velog.io/@k_hyun/Compose-%EA%B8%B0%EB%B3%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83</guid>
            <pubDate>Thu, 15 Jun 2023 11:47:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/k_hyun/post/ce89ee47-886c-4ecf-9c65-1ad8723bd5d4/image.png" alt="">
사이트를 참고하여 Compose를 활용해 위의 UI를 구성해 보았다.</p>
<h2 id="bottomnavigation">BottomNavigation</h2>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/1923f44b-58f0-45a2-93ba-5e97c6339f81/image.png" alt=""></p>
<pre><code class="language-kotlin">@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
    BottomNavigation(
        backgroundColor = MaterialTheme.colors.background,
        modifier = modifier
    ) {
        BottomNavigationItem(icon = {
            Icon(
                imageVector = Icons.Default.Spa,
                contentDescription = null
            )
        }, label = {
            Text(
                stringResource(R.string.bottom_navigation_home)
            )
        }, selected = true, onClick = { })
        BottomNavigationItem(icon = {
            Icon(
                imageVector = Icons.Default.AccountCircle,
                contentDescription = null
            )
        }, label = {
            Text(
                stringResource(R.string.bottom_navigation_profile)
            )
        }, selected = false, onClick = { })
    }
}</code></pre>
<p>BottomNavigation composable을 활용하고 그 안에 BottomNavigationItem을 선언하여 활용한다.</p>
<h2 id="대화형-다이얼로그">대화형 다이얼로그</h2>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/7bc7b2f7-3ad9-4738-8e58-50b9c42a4425/image.png" alt="">
손가락 버튼을 활용해서 Preview 화면에서도 스크롤 동작을 확인 할 수 있다.<img src="https://velog.velcdn.com/images/k_hyun/post/57989c1f-a843-470b-9f34-b1a1691b3f81/image.gif" alt=""></p>
<blockquote>
<p>참고 링크
<a href="https://developer.android.com/codelabs/jetpack-compose-layouts?hl=ko&amp;continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-1%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#11">https://developer.android.com/codelabs/jetpack-compose-layouts?hl=ko&amp;continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-1%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#11</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose Quick Summary]]></title>
            <link>https://velog.io/@k_hyun/Compose-Quick-Summary</link>
            <guid>https://velog.io/@k_hyun/Compose-Quick-Summary</guid>
            <pubDate>Wed, 14 Jun 2023 16:11:33 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtu.be/fFLBCgoHHys">https://youtu.be/fFLBCgoHHys</a></p>
<h2 id="summary">Summary</h2>
<p>Create composables using the @Composable annotation</p>
<p>It&#39;s quick &amp; easy to create composables</p>
<p>Composables accept parameters</p>
<pre><code>- Use MutableState and remember
- remember the value and not reset when recomposed
- but it wont surive configuration changes
- To remember values from across config changes, use rememberSaveable</code></pre><pre><code class="language-kotlin">// example
val selectedAnswer: Answer? by rememberSaveable { mutableStateOf&lt;Answer&gt;(null)}</code></pre>
<p>Composable should be side-effect free</p>
<h2 id="composables-can">Composables can</h2>
<p>Execute in any order
Run in parallel</p>
<pre><code>- 병렬적으로 실행</code></pre><p>Be skipped</p>
<pre><code>- 값의 변경이 있는 부분만 recompose한다.</code></pre><p>Run frequently</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose Tutorial]]></title>
            <link>https://velog.io/@k_hyun/Compose-Tutorial</link>
            <guid>https://velog.io/@k_hyun/Compose-Tutorial</guid>
            <pubDate>Wed, 14 Jun 2023 15:27:05 GMT</pubDate>
            <description><![CDATA[<h2 id="compose">Compose</h2>
<blockquote>
<p>기존의 안드로이드 UI 개발 방식인 XML 기반의 레이아웃과 달리 Compose는 Kotlin 언어를 기반으로 한 UI 작성 방식이다. 
Compose는 &quot;Composable&quot;이라 불리는 UI 구성 요소들을 조합하여 UI를 구성한다.</p>
</blockquote>
<h2 id="why-compose">Why Compose?</h2>
<h3 id="코드-감소">코드 감소</h3>
<p>XML을 작성하지 않아서 관리할 코드가 줄어든다.</p>
<p>작성자는 테스트와 디버그 작업과 버그 발생 가능성이 줄어들어 당면 문제에 집중할 수 있다. </p>
<p>검토자 또는 유지관리자는 읽고, 이해하고, 검토하고, 유지관리할 코드가 적어진다.</p>
<h3 id="직관적">직관적</h3>
<p>Compose는 선언적 API를 사용한다. 즉, Compose가 나머지를 처리하므로 UI를 설명하기만 하면 된다.</p>
<p>Compose를 사용하면 특정 활동이나 프래그먼트에 종속되지 않는 작은 스테이트리스(Stateless) 구성요소를 빌드한다.</p>
<p>이를 통해 재사용하고 테스트하기가 쉬워진다. </p>
<h3 id="빠른-개발-과정">빠른 개발 과정</h3>
<p>Compose는 기존의 모든 코드와 호환 가능하다.</p>
<p>Compose에서 Views를, Views에서 Compose 코드를 호출할 수 있다.</p>
<p>Navigation, ViewModel, Kotlin 코루틴과 같은 대부분의 일반적인 라이브러리는 Compose와 함께 작동하므로 언제 어디서든 원하는 대로 채택 가능하다.</p>
<h3 id="강력한-성능">강력한 성능</h3>
<p>머티리얼 디자인을 활용하여 애니메이션, 아름다운 디자인을 적극 활용할 수 있다.</p>
<h2 id="레이아웃-작성">레이아웃 작성</h2>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/5aabe0b3-24e7-4348-b83d-f07c79573900/image.png" alt=""></p>
<pre><code class="language-kotlin">@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard(
        msg = Message(&quot;Colleague&quot;, &quot;Hey, take a look at Jetpack Compose, it&#39;s great!&quot;)
    )
}


// Material Design 스타일 지정을 사용하여 MessageCard 컴포저블의 디자인을 개선
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,    // 다크 모드
    showBackground = true,
    name = &quot;Dark Mode&quot;
)
@Composable
fun PreviewMessageCard2() {
    ComposeTutorialTheme {
        Surface {
            MessageCard(
                msg = Message(&quot;Colleague&quot;, &quot;Take a look at Jetpack Compose, it&#39;s great!&quot;)
            )
        }
    }
}</code></pre>
<p>@Preview를 사용하면 화면 오른쪽에서 해당 코드의 디자인을 확인 할 수 있다.</p>
<p>@Composable은 해당 함수를 Compose로 표현할 수 있음을 의미한다.</p>
<p>MessageCard는 아래와 같다.</p>
<pre><code class="language-kotlin">data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
    // 전체에 패딩 8dp를 준다.
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = &quot;picture&quot;,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
        )

        // 수평으로 8dp의 공간을 준다.
        Spacer(modifier = Modifier.width(8.dp))

        Column() {
            Text(
                text = msg.author,
                color = MaterialTheme.colors.secondaryVariant,  // 글자 색
                style = MaterialTheme.typography.subtitle2  // 서체
            )
            // 수직으로 4dp의 공간을 준다.
            Spacer(modifier = Modifier.height(4.dp))

            Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
                Text(
                    text = msg.body,
                    style = MaterialTheme.typography.body2,
                    modifier = Modifier.padding(all = 4.dp)
                )
            }

        }
    }
}</code></pre>
<h2 id="목록-및-애니메이션">목록 및 애니메이션</h2>
<p>대화형 화면을 출력하는 코드는 다음과 같다.
<img src="https://velog.velcdn.com/images/k_hyun/post/e325b208-2a48-4897-89bf-d9767900bc67/image.png" alt=""></p>
<pre><code class="language-kotlin">@Composable
fun Conversation(messages: List&lt;Message&gt;) {
    LazyColumn {
        items(messages) { message -&gt;
            MessageCard(message)
        }
    }
}

@Preview
@Composable
fun PreviewConversation() {
    ComposeTutorialTheme {
        Conversation(SampleData.conversationSample)
    }
}</code></pre>
<p>대화 내용을 1줄로 줄이고 클릭시 대화 내용을 펼치는 애니메이션을 사용한 코드는 아래와 같다.
<img src="https://velog.velcdn.com/images/k_hyun/post/19804933-b46e-43ec-ba8c-3e1a5ac513c4/image.png" alt=""></p>
<pre><code class="language-kotlin">fun MessageCard(msg: Message) {
    // 전체에 패딩 8dp를 준다.
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            ...
        )

           ...

        // We keep track if the message is expanded or not in this
        // variable
        var isExpanded by remember { mutableStateOf(false) }

        // 클릭하면 isExpanded의 상태를 변경한다.
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            ...

            Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
                Text(
                    text = msg.body,
                    style = MaterialTheme.typography.body2,
                    modifier = Modifier.padding(all = 4.dp),
                    // isExpanded의 값에 따라 값을 수정한다. -&gt; 펼쳤다 줄었다 함.
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                )
            }
        }
    }
}</code></pre>
<p>펼쳤을 때 색 변경과 애니메이션을 위해 아래의 코드를 추가 작성할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/327f6c2a-396e-4564-bee3-1cf989a9e69f/image.png" alt=""></p>
<pre><code class="language-kotlin">
val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
            )

Surface(
                shape = MaterialTheme.shapes.medium,
                elevation = 1.dp,
                // surfaceColor color will be changing gradually from primary to surface
                color = surfaceColor,
                // animateContentSize will change the Surface size gradually
                modifier = Modifier.animateContentSize().padding(1.dp)
            )</code></pre>
<h2 id="추가-문법">추가 문법</h2>
<p>LazyColumn    </p>
<ul>
<li>새로로 스크롤 되는 목록을 생성</li>
</ul>
<p>LazyRow    </p>
<ul>
<li>가로로 스크롤 되는 목록을 생성</li>
</ul>
<p>remember</p>
<ul>
<li>remember는 메모리에 객체를 저장하는 API다. 계산되어 만들어진 값은 컴포지션 도중에 저장되어 리컴포지션 도중에 반환된다</li>
<li>remember로 변경 가능한 객체, 변경 불가능한 객체 모두를 저장할 수 있다</li>
<li>remember를 호출하면 UI 전체를 다시 그리는 게 아니라 변경이 필요한 부분만 변경한다.</li>
<li>mutableStateOf()라는 함수를 자주 사용해 바뀌는 값을 관리한다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[FCM (Firebase Cloud Messaging)]]></title>
            <link>https://velog.io/@k_hyun/FCM-Firebase-Cloud-Messaging</link>
            <guid>https://velog.io/@k_hyun/FCM-Firebase-Cloud-Messaging</guid>
            <pubDate>Tue, 02 May 2023 15:11:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>파이어베이스 서버에서 앱에 알림을 전달하는 기능이다.
서버에서 어떤 상황이 발생할 때 클라이언트에 데이터를 전달하는 것을 서버 푸시라고 한다.
클라우드 메시징은 서버의 데이터를 앱에 직접 전달하지 않고 FCM 서버를 거쳐 앱에 전달하는 방식이다.
서버와 앱이 네트워크 연결을 지속해서 유지하지 않아도 되며, 앱이 포그라운드 상황이 아니어도 데이터를 받을 수 있다는 장점이 있다.</p>
</blockquote>
<h2 id="원리">원리</h2>
<h3 id="토큰-발급">토큰 발급</h3>
<p>FCM 서버에 전달된 데이터를 기기의 앱에 전달하려면 앱을 구분하는 식별값이 필요하다.</p>
<p>이 식별값을 토큰이라고 하며, 안드로이드 시스템이 FCM 서버에 자동으로 의뢰해서 발급받는다.</p>
<p>과정은 다음과 같다.</p>
<ol>
<li>앱이 폰에 설치되면 안드로이드 시스템에서 FCM 서버에 토큰 발급을 요청</li>
<li>FCM 서버에서 토큰을 발급해 폰에 전달</li>
<li>전달받은 토큰은 FCM 서버에서 메시지가 발생할 때 사용되므로 서버에 전달</li>
<li>서버에서 전달받은 토큰을 DB에 저장</li>
</ol>
<h3 id="서버에서-앱으로-데이터-전송">서버에서 앱으로 데이터 전송</h3>
<ol>
<li>서버에서 특정 상황이 발생하면 DB에서 토큰을 추출해 앱을 식별한다.</li>
<li>서버에서 사용자에게 전달할 메시지와 앱을 식별할 토큰을 FCM 서버에 전달한다.</li>
<li>토큰을 분석해 해당 사용자의 폰에 메시지를 전달한다.</li>
</ol>
<h2 id="클라우드-메시징-설정">클라우드 메시징 설정</h2>
<h3 id="그래들-설정">그래들 설정</h3>
<pre><code>// 구글 서비스 등록(프로젝트 수준 그래들)
plugins {
    id &#39;com.google.gms.google-services&#39; version &#39;4.3.14&#39; apply false
}

// FCM 관련 라이브러리 등록(모듈 수준 그래들)
plugins {
    id &#39;com.google.gms.google-services&#39;
}
dependencies {
    implementation platform(&#39;com.google.firebase:firebase-bom:30.4.1&#39;)
    implementation &#39;com.google.firebase:firebase-messaging-ktx:23.0.08&#39;
    implementation &#39;com.google.firebase:firebase-analytics-ktx:21.1.1&#39;
}</code></pre><h3 id="매니페스트">매니페스트</h3>
<p>서버에서 FCM 서버에 전덜하는 정보는 토큰, 알림, 데이터로 구분된다.</p>
<pre><code>{
    notification: {
        title: &#39;...&#39;,
        body: &#39;...&#39;
    },
    data: {
        title: &#39;...&#39;,
        value: &#39;...&#39;
    },
    token: token
}</code></pre><p>notification 정보가 있으면 코드에서 알림을 발생시키지 않아도 자동으로 발생하게 할 수 있다. 그러려면 매니페스트 파일에 다음과 같은 메타 데이터를 설정해 둬야 한다.</p>
<pre><code>&lt;meta-data
    android:name=&quot;com.google.firebase.messaging.default_notification_icon&quot;
    android:resource=&quot;@drawable/ic_stat_ic_notification&quot; /&gt;
&lt;meta-data
    android:name=&quot;com.google.firebase.messaging.default_notification_color&quot;
    android:resource=&quot;@drawable/colorAccent&quot; /&gt;
&lt;meta-data
    android:name=&quot;com.google.firebase.messaging.default_notification_channel_id&quot;
    android:resource=&quot;fcm_default_channel&quot; /&gt;</code></pre><h2 id="서비스-컴포넌트-작성">서비스 컴포넌트 작성</h2>
<pre><code class="language-xml">// 서비스 등록
&lt;service
    android:name=&quot;.fcm.MyFirebaseMessageService&quot;
    ...&gt;
    &lt;intent-filter&gt;
        &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/service&gt;
</code></pre>
<pre><code class="language-kotlin">// 서비스 코드
class MyFirebaseMessageService : FirebaseMessagingService() {
    override fun onNewToken(p0: String) {...}
    override fun onMessageReceived(p0: RemoteMessage) {...}
}</code></pre>
<p>onNewToken() 함수는 FCM 서버로부터 토큰이 전달될 때 자동으로 호출되며 매개변숫값이 토큰임</p>
<p>onMessageReceived() 함수는 FM 서버에서 메시지가 전달될 때 자동으로 호출되며, 매개변수 객체의 data 프로퍼티로 메시지를 얻을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이어베이스 스토리지]]></title>
            <link>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80</link>
            <guid>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80</guid>
            <pubDate>Tue, 02 May 2023 13:17:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>파이어베이스 스토리지는 앱의 파일을 저장하는 기능을 제공한다.
앱에서 사진을 서버에 올리고 내려받을 수 있다.
<img src="https://velog.velcdn.com/images/k_hyun/post/4ba46a58-97fa-4972-bcb9-ff16d8b7deff/image.png" alt=""></p>
</blockquote>
<h2 id="파일-올리기">파일 올리기</h2>
<pre><code class="language-kotlin">// 스토리지 객체 얻기
val storage: FirebaseStorage = Firebase.storage

// 스토리지 참조 만들기
// images 폴더의 a.jpg 파일을 가리킨다
val storageRef: StorageReference = storage.reference
val imgRef: StorageReference = storageRef.child(&quot;images/a.jpg&quot;)
</code></pre>
<h3 id="putbytes를-통한-바이트값-저장">putBytes()를 통한 바이트값 저장</h3>
<p>뷰의 내용을 비트맵 -&gt; 바이트값으로 변환 후 스토리지에 저장한다.</p>
<pre><code class="language-kotlin">// 화면을 비트맵 객체에 그리기
fun getBitmapFromView(view: View): Bitmap? {
    var bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
    var canvas = Canvas(bitmap)
    view.draw(canvas)
    return bitmap
}

...

// 이미지를 바이트값으로 읽기
val bitmap = getBitmapFromView(binding.addPicImageView)
val baos = ByteArrayOutputStream()
bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val data = baos.toByteArray()

// 바이트값을 스토리지에 저장하기
var uploadTask = imgRef.putBytes(data)
uploadTask.addOnFailureListener {}.addOnCompleteListener {}</code></pre>
<h3 id="putstream-함수로-저장">putStream() 함수로 저장</h3>
<pre><code class="language-kotlin">// 파일 스트림으로 업로드
val stream = FileInputStream(File(filePath))
val uploadTask = imgRef.putStream(stream)</code></pre>
<h3 id="putfile-함수로-저장">putFile() 함수로 저장</h3>
<pre><code class="language-kotlin">// 파일 경로로 업로드
val file = Uri.fromFile(File(filePath))
val uploadTask = imgRef.putFile(file)</code></pre>
<h3 id="업로드-파일-삭제">업로드 파일 삭제</h3>
<pre><code class="language-kotlin">// 업로드 파일 삭제
val imgRef: StorageReference = storageRef.child(&quot;images/a.jpg&quot;)
imgRef.delete()
    .addOnFailureListener {...}
    .addOnCompleteListener {...}</code></pre>
<h2 id="파일-내려받기">파일 내려받기</h2>
<p>스토리지의 파일을 내려받을 때는 getBytes()나 getFile() 함수를 이용한다.</p>
<pre><code class="language-kotlin">// 내려받은 파일의 바이트값 가져오기
val storageRef: StorageReference = storage.reference
val imgRef: StorageReference = storageRef.child(&quot;images/a.jpg&quot;)
val ONE_MEGABYTE: Long = 1024 * 1024
imgRef.getBytes(ONE_MEGABYTE).addOnSuccessListener {
    val bitmap = BitmapFactory.decodeByteArray(it, 0, it.size)
    binding.downloadImageView.setImageBitmap(bitmap)
    }
    .addOnFailureListener {...}</code></pre>
<p>바이트값 -&gt; 비트맵으로 변환 후 뷰에 그린다.</p>
<h3 id="getfile-함수로-가져오기">getFile() 함수로 가져오기</h3>
<pre><code class="language-kotlin">// 로컬 저장소에 파일 내려받기
val imgRef: StorageReference = storageRef.child(&quot;images/a.jpg&quot;)
val localFile = File.createTempFile(&quot;images&quot;, &quot;jpg&quot;)
imgRef.getFile(localFile).addOnSuccessListener {
    val bitmap = BitmapFactory.decodeFile(localFile.absolutePath)
    }
    .addOnFailureListener { ... }</code></pre>
<h3 id="downloadurl-함수로-url-얻기">downloadUrl() 함수로 URL 얻기</h3>
<pre><code class="language-kotlin">// 스토리지 파일의 URL 얻기
val imgRef: StorageReference = storageRef.child(&quot;images/a.jpg&quot;)
imgRef.downloadUrl
    .addOnSuccessListener{...}
    .addOnFailureListener{...}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이어스토어 DB]]></title>
            <link>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EC%8A%A4%ED%86%A0%EC%96%B4-DB</link>
            <guid>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EC%8A%A4%ED%86%A0%EC%96%B4-DB</guid>
            <pubDate>Thu, 27 Apr 2023 13:14:40 GMT</pubDate>
            <description><![CDATA[<h2 id="파이어스토어-데이터베이스">파이어스토어 데이터베이스</h2>
<blockquote>
<p>파이어베이스는 파이어스토어 데이터베이스와 실시간 데이터베이스를 제공한다.
파이어스토어 DB는 실시간 DB보다 더 많고 빠른 쿼리를 제공한다.
NoSQL DB로, JSON형식으로 파일이 저장된다.</p>
</blockquote>
<h2 id="데이터-저장하기">데이터 저장하기</h2>
<pre><code class="language-kotlin">// 파이어스토어 객체 얻기
var db: FirebaseFireStore = FirebaseFirestore.getInstance()

// add() 함수로 데이터 저장
// colleciton() 함수는 괄호 안의 컬렉션을 사용한다는 구문이며, 없으면 새로 만든다.
val user = mapOf(
    &quot;name&quot; to &quot;user&quot;,
    &quot;email&quot; to &quot;abc@com.&quot;,
    &quot;avg&quot; to 10
    )

// 콜백 함수를 통해 성공/실패 여부를 판단.
db.collection(&quot;users&quot;)
    .add(user)
    .addOnSuccessListener{}
    .addOnFailureListener{}

// map이 아닌 객체 형식으로 저장하려면 아래와 같다.
val user2 = User(&quot;name&quot;, &quot;abc@com&quot;, 10&quot;)
db.collection(&quot;users&quot;)
    .add(user)

// set() 함수로 데이터 저장
// DocumentReferece 객체에서 함수를 제공하므로 document()로 작업 대상 문서를 지정해야 한다.
val user3 = User(&quot;lee&quot;, &quot;abc@com&quot;, 10&quot;)
db.collection(&quot;users&quot;)
    .document(&quot;ID01&quot;)
    .set(user)</code></pre>
<h2 id="데이터-업데이트--삭제">데이터 업데이트 / 삭제</h2>
<pre><code class="language-kotlin">// 특정 필드값만 업데이트
db.collection(&quot;users&quot;)
    .document(&quot;ID01&quot;)
    .update(&quot;email&quot;, &quot;lee@com&quot;)

// 여러 필드값 업데이트
db.collection(&quot;users&quot;)
    .document(&quot;ID01&quot;)
    .update(mapOf(
        &quot;name&quot; to &quot;kim&quot;
        &quot;email&quot; to &quot;kim@com&quot;
    ))

// 특정 필드값 삭제
db.collection(&quot;users&quot;)
    .document(&quot;ID01&quot;)
    .update(mapOf(
        &quot;avg&quot; to FieldValue.delete()
    ))

// 문서 전체 삭제
db.collection(&quot;users&quot;)
    .document(&quot;ID01&quot;)
    .delete()</code></pre>
<h2 id="데이터-불러오기">데이터 불러오기</h2>
<pre><code class="language-kotlin">// 전체 문서 가져오기
db.collection(&quot;users&quot;)
    .get()
    .addOnSuccessListener{}
    .addOnFailureListener{}

// 단일 문서 가져오기
val docRef = db.collection(&quot;users&quot;).document(&quot;ID01&quot;)
docRef.get()
    .addOnSuccessListener{}
    .addOnFailureListener{}

// 문서를 객체에 담기
class User{
    var name: String? = null
    var email: String? = null
    var avg: Int = 0
}
val docRef = db.collection(&quot;users&quot;).document(&quot;ID01&quot;)
docRef.get().addOnSuccessListener{ documentSnapshot -&gt;
    val selectUser = documentSnapShot.toObject(User::class.java)
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이어베이스 인증]]></title>
            <link>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@k_hyun/%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Mon, 24 Apr 2023 13:55:20 GMT</pubDate>
            <description><![CDATA[<h2 id="이메일비밀번호-인증">이메일/비밀번호 인증</h2>
<p>실제 존재하는 이메일을 등록하면 등록한 이메일로 인증 메일이 발송된다.
이를 확인함으로써 인증을 처리하는 구조이다.</p>
<p>인증을 거쳐 가입이 완료되면 인증 정보가 파이어베이스에 저장된다. 따라서 로그인할 때는 등록된 이메일 서버와 연동하지 않고 파이어베이스에서 처리한다.</p>
<h3 id="인증-사용">인증 사용</h3>
<p>파이어베이스 콘솔에서 빌드 -&gt; Authentification을 클릭한다.</p>
<p>Sign-in method에서 이메일/비밀번호 항목이 사용 설정됨이어야 한다.
<img src="https://velog.velcdn.com/images/k_hyun/post/05017f31-f493-4225-a6e7-41561afba340/image.png" alt=""></p>
<p>build.gradle 파일에 아래의 인증 라이브러리를 등록해야 한다.</p>
<pre><code>implementation &#39;com.google.firebase:firebase-auth-ktx:21.0.0&#39;</code></pre><h3 id="파이어베이스-인증-객체-얻기">파이어베이스 인증 객체 얻기</h3>
<pre><code class="language-kotlin">// 파이어베이스 인증 객체 얻기
auth = Firebase.auth</code></pre>
<h3 id="회원-가입">회원 가입</h3>
<pre><code class="language-kotlin">// 회원가입하기
auth.createUserWithEmailAndPassword(&quot;email&quot;, &quot;password&quot;)
    .addOnCompleteListener(this) { task -&gt;
        // 파이어베이스에 등록된 경우
        if (task.isSuccessful) {
            auth.currentUser?.sendEmailVerification()
                ?.addOnCompleteListener { sendTask -&gt;
                    if (sendTask.isSuccessful) {
                        // 이메일 인증이 완료 된 경우
                    } else {
                        // 그렇지 않은 경우
                    }          
        } else {
                Log.d(TAG, &quot;onCreate: failure&quot;)
        }
    }</code></pre>
<h3 id="로그인">로그인</h3>
<pre><code class="language-kotlin">// 로그인 처리
auth.signInWithEmailAndPassword(email, password)
    .addOnCompleteListener (this) { task -&gt;
        if (task.isSuccessful) {
            // 로그인 성공
        } else {
            // 로그인 실패
        }
}</code></pre>
<h3 id="정보-가져오기">정보 가져오기</h3>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/14c550c4-7e61-4d1e-8bde-70734fd57d16/image.png" alt=""></p>
<h2 id="구글-인증">구글 인증</h2>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/c2f14093-38dc-4313-8520-722f33b01c1d/image.png" alt="">
플레이 서비스 인증 라이브러리 등록은 아래와 같다.</p>
<pre><code>implementation &#39;com.google.android.gms:play-services-auth:19.2.0&#39;</code></pre><h3 id="구글-인증-처리하기">구글 인증 처리하기</h3>
<pre><code class="language-kotlin">// 구글 인증 처리
val requestLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult())
    {
        val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
        try {
                val account = task.getResult(ApiException::class.java)!!
                val credential = GoogleAuthProvider.getCredential(account.idToken, null)
                auth.signInWithCredential(credential)
                    .addOnCompleteListener (this){
                        if (task.isSuccessful) {
                            // 구글 로그인 성공
                        } else {
                            // 구글 로그인 실패
                        }
                    }
            } catch (e: ApiException) {
                // 예외 처리
            }
     }

// 구글 인증 앱 실행
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.default_web_client_id))
            .requestEmail()
            .build()

// 인텐트 객체 생성
val signInIntent = GoogleSignIn.getClient(this, gso).signInIntent
requestLauncher.launch(signInIntent)</code></pre>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/e5e6e397-952e-4d99-8bc2-5618b7a90a5e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 지도 활용]]></title>
            <link>https://velog.io/@k_hyun/%EA%B5%AC%EA%B8%80-%EC%A7%80%EB%8F%84-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@k_hyun/%EA%B5%AC%EA%B8%80-%EC%A7%80%EB%8F%84-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sat, 08 Apr 2023 02:46:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/k_hyun/post/6fb2c266-7792-4329-b7a4-88533c7dff55/image.png" alt=""></p>
<h2 id="지도-사용-설정">지도 사용 설정</h2>
<p>매니페스트 설정은 아래와 같다</p>
<pre><code>인터넷 퍼미션 사용
&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;/&gt;
구글 지도 사용 선언
&lt;uses-library android:name=&quot;org.apache.http.legacy&quot; android:required=&quot;false&quot;/&gt;
&lt;application&gt;
    http 통신을 허용하는 설정
    &lt;uses-library android:name=&quot;org.apache.http.legacy&quot; android:required=&quot;false&quot;/&gt;
    구글 맵스 API 키를 등록
    &lt;meta-data android:name=&quot;com.google.android.maps.v2.API_KEY&quot;
            android:value=&quot;## API KEY ##&quot;/&gt;
            play-services 라이브러리 버전 설정
    &lt;meta-data android:name=&quot;com.google.android.gms.version&quot;
            android:value=&quot;@integer/google_play_services_version&quot;/&gt;
&lt;/application&gt;            </code></pre><p>activity_main.xml</p>
<pre><code>지도 프래그먼트 등록
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;fragment
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:id=&quot;@+id/mapView&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:name=&quot;com.google.android.gms.maps.SupportMapFragment&quot;/&gt;</code></pre><p>MainActivity.kt</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity(), OnMapReadyCallback {
    lateinit var binding: ActivityMainBinding
    var googleMap: GoogleMap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        (supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment?)!!.getMapAsync(this)

        // 지도의 중심 이동
        val latLng = LatLng(37.566610, 126.978403)
        val position = CameraPosition.Builder()
            .target(latLng)
            .zoom(16f)
            .build()
        googleMap?.moveCamera(CameraUpdateFactory.newCameraPosition(position))

        // 마커 표시하기
        val markerOptions = MarkerOptions()
        markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_launcher_foreground))
        markerOptions.position(latLng)
        markerOptions.title(&quot;서울 시청&quot;)
        markerOptions.snippet(&quot;Tel:01-120&quot;)

        googleMap?.addMarker(markerOptions)
    }

    override fun onMapReady(p0: GoogleMap?) {

        googleMap = p0
    }
}</code></pre>
<h2 id="지도에서-사용자-이벤트-처리">지도에서 사용자 이벤트 처리</h2>
<p>GoogleMap.OnMapClickListener : 지도 클릭 이벤트
GoogleMap.OnMapLongClickListener : 지도 롱 클릭 이벤트
GoogleMap.OnMarkerClickListener : 마커 클릭 이벤트
GoogleMap.onMarkerDragListener : 마커 드래그 이벤트
GoogleMap.OnInfoWindowClickListener : 정보 창 클릭 이벤트
GoogleMap.onCameraIdleListener : 지도 화면 변경 이벤트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자 위치 얻기]]></title>
            <link>https://velog.io/@k_hyun/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9C%84%EC%B9%98-%EC%96%BB%EA%B8%B0</link>
            <guid>https://velog.io/@k_hyun/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9C%84%EC%B9%98-%EC%96%BB%EA%B8%B0</guid>
            <pubDate>Mon, 03 Apr 2023 14:31:36 GMT</pubDate>
            <description><![CDATA[<h2 id="위치-접근-권한">위치 접근 권한</h2>
<p>앱에서 사용자의 위치를 추적하려면 3가지권한을 얻어야 한다.</p>
<ul>
<li><p>android.permission.ACCESS_COARSE_LOCATION
도시에서 1블록 정도의 오차 수준을 가진다.</p>
</li>
<li><p>android.permission.ACCESS_FINE_LOCATION
최대한 정확한 위치에 접근하는 권한</p>
</li>
<li><p>android.permission.ACCESS_BACKGROUND_LOCATION
API레벨 29 이상에서 백그라운드 상태에서 위치에 접근하는 권한</p>
</li>
</ul>
<p>API레벨 31 버전부터는 ACCESS_COARSE_LOCATION과 ACCESS_FINE_LOCATION을 같이 등록해 줘야 한다.</p>
<p>서비스에서 위치 접근은 아래와 같이 등록해야 한다.</p>
<pre><code>&lt;service
    android:foregroundServiceType=&quot;location&quot;
/&gt;</code></pre><h2 id="플랫폼-api의-위치-매니저">플랫폼 API의 위치 매니저</h2>
<pre><code class="language-kotlin">// 위치 매니저 사용
val manager = getSystemService(LOCATION_SERVICE) as LocationManager</code></pre>
<h3 id="위치-제공자">위치 제공자</h3>
<ul>
<li><p>GPS
GPS 위성을 이용한다.</p>
</li>
<li><p>NetWork
이동 통신망을 이용한다.</p>
</li>
<li><p>Wifi
와이파이를 이용한다.</p>
</li>
<li><p>Passive
다른 앱에서 이용한 마지막 위치 정보를 이용한다.</p>
</li>
</ul>
<pre><code class="language-kotlin">// 모든 위치 제공자 알아보기
// 지금 사용할 수 있는 위치 제공자를 알아보려면 all Providers 대신 getProviders(true)를 사용한다.
var result = &quot;All Providers : &quot;
val providers = manager.allProviders
for (provider in providers) {
    result += &quot;$provider, &quot;
    }    </code></pre>
<h3 id="위치-정보-얻기">위치 정보 얻기</h3>
<p>위치를 한 번만 얻을 땐 getLastKnownLocation() 함수를 사용한다.</p>
<pre><code class="language-kotlin">val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
location?.let {
    // 위도
    val latitude = location.latitude
    // 경도
    val longitude = location.longitude
    // 정확도
    val accuray = location.accuray
    // 획득 시간
    val time = location.time
    }</code></pre>
<p>계속 위치를 가져와야 한다면 LocationListener를 이용한다.</p>
<pre><code class="language-kotlin">val listener: LocationListener = object: LocationListener {
    // 새 위치를 가져오면 호출된다.
    override fun onLocationChanged(location: Location) {}
    // 위치 제공자가 이용할 수 있는 상황이면 호출된다.
    override fun onProviderDisabled(provider: String) {}
    // 위치 제공자가 이용할 수 없는 상황이면 호출된다.
    override fun onProviderEnabled(provider: String) {}
}
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
...
manager.removeUpdates(listener)</code></pre>
<h2 id="구글-play-서비스-위치-라이브러리">구글 Play 서비스 위치 라이브러리</h2>
<p>구글에서는 최적의 알고리즘으로 위치 제공자를 지정할 수 있도록 Fused Location Provider 라이브러리를 제공한다.</p>
<p>play 서비스 사용 선언</p>
<pre><code>implementation &#39;com.google.android.gms:play=services:12.0.1&#39;</code></pre><p>Fused Location Provider의 핵심 클래스는 2가지이다.</p>
<ul>
<li><p>FusedLocationProviderClient
위치 정보를 얻는다.</p>
</li>
<li><p>GoogleApiClient
위치 제공자 준비 등 다양한 콜백을 제공한다.</p>
</li>
</ul>
<p>GoogleApiClient에서 위치 정보 제공자를 결정하면 이를 이용해서 FusedLocationProviderClient에서 위치를 가져오는 구조이다.</p>
<pre><code class="language-kotlin">// GoogleApiClient 초기화
val connectionCallback = object: GoogleApiClient.ConnectionCallbacks {
    // 위치 제공자를 사용할 수 있을 때 위치 획득
    override fun onConnected(p0: Bundle?) {}
    // 위치 제공자를 사용할 수 없을 때
    override fun onConnectionSuspended(p0: Int) {}

val onConnectionFailedCallback = object : GoogleApiClient.OnConnectionFailedListener {
    // 사용할 수 있는 위치 제공자가 없을 때
    override fun onConnectionFailed(p0: ConnectionResult) {}
    }

val apiClient: GoogleApiClient = GoogleApiClient.Builder(this)
    .addApi(LocationServices.API)
    .addConnectionCallbacks(connectionCallback)
    .addOnConnectionFailedListener(onConnectionFailedCallback)
    .build()

// FusedLocationProviderClient 초기화
val providerClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)

// 위치 제공자 요청
apiClient.connect()    </code></pre>
<p>connect() 함수를 호출하면 여러 가지 상황을 고려해 가장 알맞은 위치 제공자를 선택 후, onConnected() 함수를 호출해 준다.</p>
<p>따라서 onConnected() 함수에서 FusedLocationProviderClient의 getLastLocation() 함수만 호출해 주면 된다.</p>
<pre><code class="language-kotlin">// 사용자 위치 얻기
override fun onConnected(p0: Bundle?) {
    ...
    providerClient.getLastLocation().addOnSuccessListener(
        this@FusedActivity,
        object : OnSuccessListener&lt;Location&gt; {
            override fun onSuccess(location: Location?) {
                val latitude = location?.latitude
                val longitude = location?.longitude
                }
            })
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 처리 - Glide]]></title>
            <link>https://velog.io/@k_hyun/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-Glide</link>
            <guid>https://velog.io/@k_hyun/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-Glide</guid>
            <pubDate>Wed, 29 Mar 2023 13:58:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>서버에서 이미지를 내려받을 때 Glide를 이용하면 Volley나 Retrofit보다 더 쉽고 빠르게 개발할 수 있다.</p>
</blockquote>
<p>Glide를 사용하려면 다음을 작성해야한다.</p>
<pre><code>implementation &#39;com.github.bumptech.glide:glide:4.13.2&#39;</code></pre><h2 id="이미지를-가져와-출력">이미지를 가져와 출력</h2>
<p>load() 함수에 리소스를 전달하고, into()함수에 이미지 뷰 객체를 전달하면 리소스 이미지를 자동으로 가져와 출력한다.</p>
<pre><code class="language-kotlin">// 리소스 이미지 출력
Glide.with(this)
    .load(R.drawable.seoul)
    .into(binding.resultView)

// 파일 이미지 출력    
val requestLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult())
{
    Glide.with(this)
        .load(it.data!!.data)
        .into(binding.resultView)
}
val intent = Intent(Intent.ACTION_PICK, MediaStroe.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = &quot;image/*&quot;
requestLauncher.launch(intent)

// 서버 이미지 출력
Glide.with(this)
    .load(url)
    .into(binding.resultView)
</code></pre>
<p>Glide를 이용하면 특별히 처리하지 않아도 이미지 뷰에 애니메이션이 적용된 GIF 이미지를 출력할 수 있다.</p>
<h2 id="크기-조절">크기 조절</h2>
<p>Glide를 이용하면 코드에서 이미지 크기를 줄이지 않아도 이밎 뷰의 크기에 맞게 자동으로 줄여서 불러온다. 따라서 OOM 문제를 크게 신경 쓰지 않아도 된다.</p>
<p>특정한 크기로 이미지를 줄이고 싶다면 override() 함수를 사용한다.</p>
<pre><code class="language-kotlin">// 크기 조절
Glide.with(this)
    .load(R.drawble.seoul)
    .override(200, 200)
    .into(binding.resultView)</code></pre>
<h2 id="로딩-오류-이미지-출력">로딩, 오류 이미지 출력</h2>
<pre><code class="language-kotlin">// 로딩, 오류 이미지 출력
Glide.with(this)
    .load(url)
    .override(200, 200)
    .placeholder(R.drawable.loading)
    .error(R.drawable.error)
    .into(binding.resultView)</code></pre>
<h2 id="이미지-데이터-사용하기">이미지 데이터 사용하기</h2>
<pre><code class="language-kotlin">Glide.with(this)
    .load(url)
    .into(object : CustomTarget&lt;Drawable&gt; () {
        override fun onResourceReady(
            resource: Drawable,
            transition: Transition&lt;in Drawable&gt;?
        ) {

        }
        override fun onLoadCleared(placeholder: Drawable?) {

        }
    })</code></pre>
<p>into() 함수에 이미지 데이터를 받을 객체를 지정했다.
이 객체는 CustomTarget을 상속받아 onResourceReady()와 onLoadCleared() 함수를 재정의해야 한다.
load() 함수에 명시한 이미지를 불러왔을 때 onResourceReady() 함수가 자동으로 호출되며, 매개변수로 이미지를 Drawable 타입으로 전달해 준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Retrofit]]></title>
            <link>https://velog.io/@k_hyun/Retrofit</link>
            <guid>https://velog.io/@k_hyun/Retrofit</guid>
            <pubDate>Wed, 29 Mar 2023 12:51:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>스퀘어에서 만든 HTTP 통신을 간편하게 만들어 주는 라이브러리이다.</p>
</blockquote>
<p>동작 방식은 아래와 같다.</p>
<ol>
<li>통신용 함수를 선언한 인터페이스를 작성한다.</li>
<li>Retrofit에 인터페이스를 전달한다.</li>
<li>Retrofit이 통신용 서비스 객체를 반환한다.</li>
<li>서비스의 통신용 함수를 호출 후 Call 객체를 반환한다.</li>
<li>Call 객체의 enqueue() 함수를 호출하여 네트워크 통신을 수행한다.</li>
</ol>
<h3 id="라이브러리-선언">라이브러리 선언</h3>
<pre><code>implementation &#39;com.squareup.retrofit2:retrofit:2.9.0&#39;
implementation &#39;com.google.code.gson:gson:2.8.6&#39;
implementation &#39;com.squareup.retrofit2:converter-gson:2.9.0&#39;</code></pre><p>Retrofit은 JOSN이나 XML 데이터를 모델 객체로 변환해 주는데, 이때 gson 라이브러리 및 컨버터를 사용한다.</p>
<h3 id="모델-클래스-선언">모델 클래스 선언</h3>
<p>모델 클래스는 서버와 주고받는 데이터를 표현하는 클래스이다.
VO (value-object) 클래스라고도 한다.</p>
<p>원래는 JSON 데이터를 코드에서 직접 파싱해서 이용해야 하지만, 데이터를 담을 모델 클래스를 선언하고 클래스 정보만 알려주면 모델 클래스의 객체를 알아서 생성하고 그 객체에 데이터를 담아준다.</p>
<pre><code>{
    &quot;id&quot;: 7,
    &quot;email&quot;: &quot;abcde@gmail.com&quot;,
    &quot;first_name&quot;: &quot;hk&quot;,
    &quot;last_name&quot;: &quot;lee&quot;
}</code></pre><p>위의 JSON 정보를 담을 모델 클래스는 아래와 같이 작성할 수 있다.</p>
<pre><code class="language-kotlin">data class UserModel(
    var id: String,
    @SerializedName(&quot;first_name&quot;)
    var firstName: String,
    var lastName: String
)

// 아래와 같이 클래스를 분리해서 작성할 수 있다.
data class UserListModel(
    var page: String,
    var perPage: String,
    var total: String,
    var totalPages: String,
    var data: List&lt;UserModel&gt;?

)</code></pre>
<p>@SerializedName 애너테이션을 통해 first_name이라는 키의 데이터가 firstName에 저장된다고 표시한다.
email 프로퍼티는 존재하지 않지만, 문제는 없다.
키가 last_name이면 자동으로 lastName 프로퍼티에 저장된다.</p>
<h3 id="서비스-인터페이스-정의">서비스 인터페이스 정의</h3>
<p>네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 서비스 인터페이스를 작성해야 한다.</p>
<pre><code class="language-kotlin">interface INetworkService {
    @GET(&quot;api/users&quot;)
    fun doGetUserList(@Query(&quot;page&quot;) page: String): Call&lt;UserModel&gt;
    @GET
    fun getAvatarImage(@Url url:String): Call&lt;ResponseBody&gt;
}</code></pre>
<p>이 인터페이스를 구현해 실제로 통신하는 클래스는 Retrofit이 자동으로 만들어 준다. 이때 애너테이션을 참조한다.</p>
<p>@GET은 서버와 연동할 때 GET 방식으로 해달라는 의미이다.
@Query는 서버에 전달되는 데이터
@Url은 요청 URL을 의미한다.</p>
<h3 id="retrofit-객체-생성">Retrofit 객체 생성</h3>
<pre><code class="language-kotlin">val retrofit: Retrofit
    get() = Retrofit.Builder()
        .baseUrl(&quot;https://reqres.in/&quot;)
        .addConverterFactory(GsonConverterFactory.create())
        .build()</code></pre>
<p>baseUrl을 위 처럼 선언하고 @GET(&quot;api/users&quot;)처럼 경로를 지정한다면 서버 요청 URL은 &quot;<a href="https://reqres.in/api/users&quot;">https://reqres.in/api/users&quot;</a> 가 된다.</p>
<h3 id="인터페이스-타입의-서비스-객체-얻기">인터페이스 타입의 서비스 객체 얻기</h3>
<pre><code class="language-kotlin">var networkService: INetworkService = retrofit.create(INetWorkService::class.java)</code></pre>
<p>Retrofit의 create()함수에 앞에서 만든 서비스 인터페이스 타입을 전달한다.</p>
<p>그러면 이 인터페이스를 구현한 클래스의 객체를 반환해준다.</p>
<h3 id="네트워크-통신-시도">네트워크 통신 시도</h3>
<pre><code class="language-kotlin">// Call 객체 얻기
val userListCall = networkService.doGetUserList(&quot;1&quot;)</code></pre>
<p>인터페이스에 선언한 함수를 호출하면 Call 객체가 반환된다.
실제 통신은 이 Call 객체의 enqueue() 함수를 호출하는 순간 이뤄진다.</p>
<pre><code class="language-kotlin">userListCall.enqueue(object : Callback&lt;UserListModel&gt; {
    override fun onResponse(call: Call&lt;UserListModel&gt;, response: Response&lt;UserListModel&gt;) {}
    override fun onFailure(call: Call&lt;UserListModel&gt;, t: Throwable) {}</code></pre>
<p>통신이 성공하면 onResponse() 함수가, 실패하면 onFailure() 함수가 호출된다.</p>
<h2 id="retrofit-애너테이션">Retrofit 애너테이션</h2>
<h3 id="get-post-put-delete-head">@GET, @POST, @PUT, @DELETE, @HEAD</h3>
<pre><code class="language-kotlin">@GET(&quot;users/list?sort=desc&quot;)
fun test1(): Call&lt;UserModel&gt;

val call: Call&lt;UserModel&gt; = networkService.test1()</code></pre>
<h3 id="path">@Path</h3>
<pre><code class="language-kotlin">@GET(&quot;group/{id}/users/{name}&quot;)
fun test2(
    @Path(&quot;id&quot;) userId: String,
    @Path(&quot;name&quot;) arg2: String,
): Call&lt;UserModel&gt;

val call: Call&lt;UserModel&gt; = networkService.test2(&quot;10&quot;, &quot;lee&quot;)
</code></pre>
<h3 id="query">@Query</h3>
<pre><code class="language-kotlin">@GET(&quot;group/users&quot;)
fun test3(
    @Query(&quot;sort&quot;) arg1: String,
    @Query(&quot;name&quot;) arg2: String
): Call&lt;UserModel&gt;

val call: Call&lt;UserModel&gt; = networkService.test3(&quot;age&quot;, &quot;lee&quot;)

// 서버 요청 URL
https://reqres.in/group/users?sort=age&amp;name=lee</code></pre>
<h3 id="querymap">@QueryMap</h3>
<pre><code class="language-kotlin">@GET(&quot;group/users&quot;)
fun test4(
    @QueryMap options: Map&lt;String, String&gt;,
    @Query(&quot;name&quot;) name: String
): Call&lt;UserModel&gt;

val call: Call&lt;UserModel&gt; = networkService.test4(
        mapOf&lt;String, String&gt; (&quot;one&quot; to &quot;hello&quot;, &quot;two&quot; to &quot;world&quot;),
        &quot;lee&quot;
)

// 서버 요청 URL
https://reqres.in/group/users?one=hello&amp;two=world&amp;name=lee</code></pre>
<h3 id="body">@Body</h3>
<pre><code class="language-kotlin">@POST(&quot;group/users&quot;)
fun test5(
    @Body user: UserModel,
    @Query(&quot;name&quot;) name: String
): Call&lt;UserModel&gt;

val call: Call&lt;UserModel&gt; = networkService.test5(
    UserModel(id=&quot;1&quot;, firstName=&quot;gildong&quot;, lastName=&quot;hong&quot;),
    &quot;lee&quot;
)    </code></pre>
<p>@Body 애너테이션을 사용하면 서버 요청 URL은 바뀌지 않는다.
@Body로 지정한 모델의 데이터는 데이터 스트림으로 서버에 전송된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 통신, Volley]]></title>
            <link>https://velog.io/@k_hyun/HTTP-%ED%86%B5%EC%8B%A0-Volley</link>
            <guid>https://velog.io/@k_hyun/HTTP-%ED%86%B5%EC%8B%A0-Volley</guid>
            <pubDate>Tue, 28 Mar 2023 13:45:41 GMT</pubDate>
            <description><![CDATA[<p>앱에서 네트워크 통신을 구현하려면 아래 퍼미션을 선언해야 한다.</p>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;</code></pre><p>안드로이드 앱은 네트워크 통신을 할 때 기본으로 HTTPS 보안 프로토콜을 사용한다.</p>
<p>일반 HTTPS 프로토콜로 통신하려면 특정 도메인만 허용하도록 선언해 줘야 한다.
res/xml 폴더에 임의의 이름으로 XML파일을 만들고 다음처럼 작성한다.</p>
<pre><code>&lt;network-security-config&gt;
    &lt;domain-config cleartextTrafficPermitted=&quot;true&quot;&gt;
        &lt;domain includeSubdomains=&quot;true&quot;&gt;HTTP 프로토콜로 접속 허용할 IP, 도메인 &lt;/domain&gt;
    &lt;/domain-config&gt;
&lt;network-security-config&gt;    </code></pre><p>위의 파일을 매니페스트에 등록해서 해당 도메인에 한해서만 HTTP 통신이 가능하다.</p>
<pre><code>&lt;application
    ...
    android:networkSecurityConfig=&quot;@xml/network_security_config&quot;&gt;    </code></pre><p>또는 매니페스트에서 usesCleartextTraffic 속성을 true로 설정하면 앱 전체에서 모든 도메인의 서버와 HTTP 통신을 할 수 있다.</p>
<pre><code>&lt;application
    android:usesCleartextTraffic=&quot;true&quot;&gt;</code></pre><h2 id="volley-라이브러리">Volley 라이브러리</h2>
<blockquote>
<p>HTTP 통신을 좀 더 쉽게 구현하게 해주는 라이브러리</p>
</blockquote>
<p>Volley 라이브러리 등록</p>
<pre><code>implementation &#39;com.android.volley:volley:1.2.1&#39;</code></pre><p>핵심 클래스는 RequestQueue와 XXXRequest 이다.</p>
<ul>
<li>RequestQueue : 서버 요청자</li>
<li>XXXRequest : XXX 타입의 결과를 받는 요청 정보</li>
</ul>
<h3 id="문자열-데이터-요청">문자열 데이터 요청</h3>
<pre><code class="language-kotlin">// 문자열 요청 정의
val stringRequest = StringRequest(
                RequestMethod.POST,
                url,
                Response.Listener&lt;String&gt; {},
                Response.ErrorListener {}
                ) {
                override fun getParams(): MutableMap&lt;String, String&gt; {
                    return mutableMapOf&lt;String, String&gt;(&quot;one&quot; to &quot;hello&quot;, &quot;two&quot; to &quot;world&quot;)
                    }
                }
// 서버에 요청하기
val queue = Volley.newRequestQueue(this)
queue.add(stringRequest)</code></pre>
<p>POST방식으로 서버에 요청할 때는 getParams() 함수를 재정의해서 작성하면 자동으로 호출된다.</p>
<h3 id="이미지-데이터-요청">이미지 데이터 요청</h3>
<pre><code class="language-kotlin">val imageRequest = ImageRequest(
                url,
                Response.Listener { response -&gt; binding.imageView.setImageBitmap(response) },
                0,
                0,
                ImageView.ScaleType.CENTER_CROP,
                null,
                Response.ErrorListener {}
                )

val queue = Volley.netRequestQueue(this)
queue.add(imageRequest)</code></pre>
<h3 id="화면-출력용-이미지-데이터-요청하기">화면 출력용 이미지 데이터 요청하기</h3>
<p>서버에서 가져온 이미지를 화면에 출력만 한다면 NetworkImageView가 더 편리하다.</p>
<pre><code>&lt;com.android.volley.toolbox.NetworkImageView
    android:id=&quot;@+id/networkImageView&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot; /&gt;</code></pre><p>위의 뷰를 작성하고 이 객체의 setImageUrl() 함수만 호출하면 서버에서 가져온 이미지를 출력하는 것까지 자동으로 이뤄진다.</p>
<p>RequestQueue의 add()함수를 호출하지 않아도 setImageUrl()을 통해 서버에 요청할 수 있다.</p>
<pre><code class="language-kotlin">val queue = Volley.newRequestQueue(this)
val imgMap = HashMap&lt;String, Bitmap&gt;()
imageLoader = ImageLoader(queue, object : ImageLoader.ImageCache {
    override fun getBitmap(url: String): Bitmap? {
        return imgMap[url]
    }
    override fun putBitmap(url: String, bitmap: Bitmap) {
        imgMap[url] = bitmap
    }
})
binding.networkImageView.setImageUrl(url, imageLoader)</code></pre>
<p>setImageUrl을 사용하면 서버 이미지를 가져오기 전에 getBitmap이 호출된다. 이 함수의 반환값이 널이면 서버로 이미지를 불러들어와 putBitmap() 함수가 호출된다. 서버 이미지를 putBitmap()두 번째 매개변수로 전달해준다.</p>
<h3 id="json-데이터-요청하기">JSON 데이터 요청하기</h3>
<pre><code class="language-kotlin">// JSON 데이터 요청
val jsonRequest =
    jsonObjectRequest(
        Request.Method.GET,
        url,
        null,
        Response.Listener&lt;JSONObject&gt; { response -&gt;
            val title = response.getString(&quot;title&quot;)
            val date = response.getString(&quot;date&quot;)
        },
        Response.ErrorListener {}
        )
val queue = Volley.newRequestQueue(this)
queue.add(jsonRequest)

// 서버로부터 아래의 JSON 데이터가 전달되었다는 가정 아래 작성한 코드이다.
{
    &quot;title&quot;: &quot;제목&quot;,
    &quot;date&quot;: &quot;2023-03-28&quot;
}</code></pre>
<pre><code class="language-kotlin">// JSON 배열 요청
val jsonArrayRequest = JsonArrayRequest(
    Request.Method.GET,
    url,
    null,
    Response.Listener&lt;JSONArray&gt; { response -&gt;
        for (i in 0 until response.length()) {
            val jsonObject = response[i] as JSONObject
            val title = jsonObject.getString(&quot;title&quot;)
            val date = jsonObject.getString(&quot;date&quot;)
            }
    },
    Response.ErrorListener {}
)
val queue = Volley.newRequestQueue(this)
queue.add(jsonArrayRequest)
// 아래와 같은 JSON 데이터가 전달된다는 가정 아래 작성
[
    {
        &quot;title&quot;: &quot;제목&quot;,
        &quot;date&quot;: &quot;2023-03-28&quot;
    },
    {
        &quot;title&quot;: &quot;제목&quot;,
        &quot;date&quot;: &quot;2023-03-28&quot;
    }
]
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[스마트폰 정보]]></title>
            <link>https://velog.io/@k_hyun/%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%B0-%EC%A0%95%EB%B3%B4</link>
            <guid>https://velog.io/@k_hyun/%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%B0-%EC%A0%95%EB%B3%B4</guid>
            <pubDate>Mon, 27 Mar 2023 13:29:46 GMT</pubDate>
            <description><![CDATA[<h2 id="전화-상태-변화-감지">전화 상태 변화 감지</h2>
<p>전화가 걸려오는 순간, 서비스 상태가 변경되는 순간을 감지하고 싶은 경우가 있다.</p>
<p>스마트폰의 상태를 파악하는 방법은 두가지 존재한다.</p>
<h3 id="phonestatelistener">PhoneStateListener</h3>
<p>API레벨 31에서 deprecated 되었다.</p>
<p>PhoneStateListener를 상속받은 클래스의 객체를 TelephonyManager에 등록해서 사용한다.
그러면 스마트폰의 전화 관련 상태가 바뀔 때마다 PhoneStateListener의 다음 함수가 자동으로 호출된다.</p>
<ul>
<li>onServiceStateChanged</li>
<li>onCallForwardingIndicatorChanged</li>
<li>onDataActivity
... 등 여러 함수가 존재하나, 필요한 함수만 재정의해 놓으면 앱에서 변화를 감지한다.</li>
</ul>
<pre><code class="language-kotlin">// 상태 변화 감지
val phoneStateListener = object : PhoneStateListener() {
    override fun onServiceStateChanged(serviceState: ServiceState?) {
        super.onServiceStateChanged(serviceState)
        ...
    }
}    

// 전화 매니저 얻기
val manager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
manager.listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE)

// 상태 감지 해제
manager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)</code></pre>
<p>listen() 함수의 두 번째 매개변수에는 감지할 상태를 지정한다.</p>
<h3 id="telephonycallback">TelephonyCallback</h3>
<p>12버전 부터는 스마트폰의 상태 변화를 감지할 때 TelephonyCallback을 이용한다.
TelephonyCallback을 구현한 객체를 TelephonyManager에 등록하면 TelephonyCallback의 함수가 자동으로 호출된다.</p>
<pre><code class="language-kotlin">telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
    telephonyManager.registerTelephonyCallback(
        mainExecutor,
        object : TelephonyCallback(), TelephonyCallback.CallStateListener {
            override fun onCallStateChanged(state: Int) {
                ...
            }
        }
    )
}    </code></pre>
<p>TelephonyCallback을 TelephonyManager에 등록할 때는 registerTelephonyCallback() 함수를 이용한다.</p>
<p>위 코드가 정상적으로 실행되려면 아래의 퍼미션을 등록해야 한다.</p>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.READ_PHONE_STATE&quot; /&gt;</code></pre><h2 id="네트워크-제공-국가-사업자-전화번호">네트워크 제공 국가, 사업자, 전화번호</h2>
<p>TelephonyManager는 네트워크 제공 국가, 사업자, 전화번호를 반환하는 함수를 제공한다.</p>
<ul>
<li>getNetworkContryIso() : 네트워크 제공 국가</li>
<li>getNetworkOperatorName() : 네트워크 제공 사업자</li>
</ul>
<p>사용자 전화번호를 추출하려면 아래 퍼미션이 필요하다.</p>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.READ_PHONE_NUMBERS&quot; /&gt;</code></pre><p>사용자 스마트폰의 전화번호를 추출하는 방법은 33 버전부터는 SubscriptionManager의 getPhoneNumber()가 권장된다.</p>
<p>이전으로는 TelephonyManager의 getLine1Number() 함수를 사용한다.</p>
<h2 id="네트워크-접속-정보">네트워크 접속 정보</h2>
<p>네트워크 접속 정보를 파악할 때는 ConnectivityManager를 이용한다.</p>
<p>아래 퍼미션을 선언해야 한다.</p>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot; /&gt;</code></pre><h3 id="getactivenetwork">getActiveNetwork()</h3>
<p>API 레벨 23 이후로는 ConnectivityManager의 getActiveNetwork() 함수로 Network 객체를 얻어서 이용한다.</p>
<p>하위 버전은 ConnectivityManager의 getActiveNetworkInfo() 함수를 이용해 NetworkInfo 객체를 얻어야 한다.</p>
<h3 id="requestnetwork">requestNetwork()</h3>
<p>ConnectivityManager 클래스의 requestNetwork() 함수를 이용할 수 있다.</p>
<p>아래의 퍼미션이 필요하다.</p>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.CHANGE_NETWORK_STATE&quot; /&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Shared Preference]]></title>
            <link>https://velog.io/@k_hyun/Shared-Preference</link>
            <guid>https://velog.io/@k_hyun/Shared-Preference</guid>
            <pubDate>Sat, 25 Mar 2023 09:42:45 GMT</pubDate>
            <description><![CDATA[<h2 id="공유된-프리퍼런스">공유된 프리퍼런스</h2>
<blockquote>
<p>플랫폼 API에서 제공하는 클래스로, 데이터를 키-값 형태로 저장할 때 사용한다.
간단한 데이터를 저장하는데 유용하며, 내장 메모리의 앱 폴더에 XML 파일로 저장된다.</p>
</blockquote>
<pre><code class="language-kotlin">// 액티비티의 데이터 저장
val sharedPref = getPreferences(Context.MODE_PRIVATE)

// 앱 전체의 데아터 저장
val sharedPref = getSharedPreferences(&quot;my_prefs&quot;, Context.MODE_PRIVATE)</code></pre>
<p>getPreferences() 함수는 자동으로 이를 호출한 액티비티이름.xml 파일이 생성되고 데이터가 저장된다.</p>
<p>getSharedPreferences()는 첫 번째 매개변수로 작성한 이름의 파일로 데이터가 저장된다.</p>
<pre><code class="language-kotlin">// 프리퍼런스에 데이터 저장
sharedPref.edit().run {
        putString(&quot;data1&quot;, &quot;hello&quot;)
        putInt(&quot;data2&quot;, 10)
        commit()
    }

// 데이터 가져오기
val data1 = sharedPref.getString(&quot;data1&quot;, &quot;world&quot;)
val data2 = sharedPref.getInt(&quot;data2&quot;, 10)</code></pre>
<p>위치는 data / data / 패키지명 / shared_prefs 안에 저장된다.
<img src="https://velog.velcdn.com/images/k_hyun/post/65f3ef53-0068-4cab-abfb-c8e1486144f6/image.png" alt=""></p>
<h2 id="앱-설정-화면">앱 설정 화면</h2>
<p>앱의 설정 화면은 액티빝, 사용자 이벤트 처리, 공유된 프리퍼런스 등을 이용해서 구현한다.
화면이나 설정한 데이터를 저장하는 형태는 비슷하므로, 많은 앱에서는 설정 화면을 자동으로 만들어 주는 API를 이용한다.</p>
<p>AndroidX의 Preference를 이용할 것을 권장하고 있다.
이를 사용하려면 빌드 그래들 파일에 아래의 라이브러리를 dependencies로 선언해야 한다.</p>
<pre><code>implementation (&#39;androidx.preference:preference:1.2.0&#39;) {
        // 충돌 문제로 아래 2개의 모듈을 제외한다.
        exclude group: &#39;androidx.lifecycle&#39;, module:&#39;lifecycle-viewmodel&#39;
        exclude group: &#39;androidx.lifecycle&#39;, module:&#39;lifecycle-viewmodel-ktx&#39;
    }</code></pre><h3 id="프리퍼런스-이용-방법">프리퍼런스 이용 방법</h3>
<p>res/xml 디렉터리에 설정과 관련된 XML 파일을 만들어야 한다.</p>
<pre><code>// 설정 XML 파일
&lt;PreferenceScreen xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;SwitchPreferenceCompat
        android:key=&quot;notifications&quot;
        android:title=&quot;Enable message notifications&quot;/&gt;
    &lt;Preference
        android:key=&quot;feedback&quot;
        android:title=&quot;Send feedback&quot;
        android:summary=&quot;Report technical issues or suggest new features&quot;/&gt;
&lt;/PreferenceScreen&gt;</code></pre><p>설정 XML파일을 적용할 프래그먼트 클래스</p>
<pre><code class="language-kotlin">class MySettingFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.settings, rootKey)
    }
}</code></pre>
<p>액티비티에서 프래그먼트 출력</p>
<pre><code>&lt;FrameLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;&gt;
        &lt;fragment
            android:id=&quot;@+id/frag&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            class=&quot;com.example.test17.MySettingFragment&quot;/&gt;
&lt;/FrameLayout&gt;</code></pre><p><img src="https://velog.velcdn.com/images/k_hyun/post/196fffd4-1491-4444-9d7b-3405d88bdb43/image.png" alt=""></p>
<h3 id="설정-화면-구성">설정 화면 구성</h3>
<pre><code>&lt;PreferenceScreen xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;PreferenceCategory
        android:key=&quot;a_category&quot;
        android:title=&quot;A Setting&quot;&gt;
        &lt;SwitchPreferenceCompat
            android:key=&quot;a1&quot;
            android:title=&quot;A - 1 Setting&quot; /&gt;
        &lt;SwitchPreferenceCompat
            android:key=&quot;a2&quot;
            android:title=&quot;A - 2 Setting&quot; /&gt;
    &lt;/PreferenceCategory&gt;
    &lt;PreferenceCategory
        android:key=&quot;B_category&quot;
        android:title=&quot;B setting&quot;&gt;
        &lt;SwitchPreferenceCompat
            android:key=&quot;b1&quot;
            android:title=&quot;B - 1 Setting&quot;/&gt;
    &lt;/PreferenceCategory&gt;
&lt;/PreferenceScreen&gt;</code></pre><p>카테고리를 나누어서 항목별 출력이 가능하다.
<img src="https://velog.velcdn.com/images/k_hyun/post/e06f52c3-7012-452c-8b17-967a04cb7a52/image.png" alt=""></p>
<h3 id="포함하는-설정-화면">포함하는 설정 화면</h3>
<pre><code>&lt;PreferenceScreen xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;
    &lt;Preference
        android:key=&quot;a&quot;
        android:summary=&quot;A setting summary&quot;
        android:title=&quot;A Setting&quot;
        app:fragment=&quot;com.example.test17.ASettingFragment&quot;/&gt;

    &lt;Preference
        android:key=&quot;b&quot;
        android:summary=&quot;B setting summary&quot;
        android:title=&quot;B Setting&quot;
        app:fragment=&quot;com.example.test17.BSettingFragment&quot;/&gt;

&lt;/PreferenceScreen&gt;</code></pre><p>Preference 태그를 이용해 설정 화면을 분할했다면 액티비티에서 PreferenceFragmentCompat.OnPreferenceStartFragmentCallback 인터페이스를 구현하고 onPreferenceStartFragment() 함수를 재정의 해서 작성해야 한다.
뒤로가기 버튼을 눌렀을 때 이전 설정 화면이 나오지 않는 문제가 발생하기 때문이다.</p>
<p>이러한 과정이 복잡하다면 메인 설정 화면에서 인텐트를 이용해 하위 설정 화면을 띄우는 방법으로 구현 가능하다.
즉, 설정 화면의 항목을 사용자가 클릭했을 때 다른 액티비티를 실행하는 기능을 XML 설정만으로 구현할 수 있다.</p>
<pre><code>&lt;Preference
    app:key=&quot;activity&quot;
    app:title=&quot;Launch activity&gt;
    &lt;intent
        android:targetClass=&quot;com.example.test17.SomeActivity&quot;
        android:targetPackage=&quot;com.example.test17&quot;&gt;
           &lt;extra
            android:name=&quot;example_key&quot;
            android:value=&quot;example_value&quot; /&gt;
    &lt;/intent&gt;
&lt;/Preference&gt;     </code></pre><h2 id="설정-제어">설정 제어</h2>
<p>사용자가 설정 항목을 클릭한 순간의 이벤트를 처리하거나 설정 항목 옆에 나타나게 하는 방법
즉 코드에서 설정을 제어하는 방법이다.</p>
<p>글 입력받기</p>
<pre><code>&lt;EditTextPreference
        android:key=&quot;id&quot;
        android:title=&quot;ID 설정&quot;
        app:isPreferenceVisible=&quot;false&quot;/&gt;</code></pre><pre><code class="language-kotlin">// 설정값을 코드에서 바꾸기
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.settings, rootKey)
        val idPreference: EditTextPreference? = findPreference(&quot;id&quot;)
        idPreference?.isVisible = true

        idPreference?.summary = &quot;code summary&quot;
        idPreference?.title = &quot;code title&quot;
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/0a96c46b-8411-4cd8-bf1e-77f937567568/image.png" alt=""></p>
<p>EditTextPreference나 ListPreference는 설정값을 summary 속성에 자동으로 지정해야 할 수도 있다.
이때 SimpleSummaryProvider를 사용한다.</p>
<p>설정 XML예</p>
<pre><code>&lt;EditTextPreference
        android:key=&quot;id&quot;
        android:title=&quot;ID 설정&quot; /&gt;

&lt;ListPreference
    android:key=&quot;color&quot;
    android:title=&quot;색상 선택&quot;
    android:entries=&quot;@array/my_color&quot;
    android:entryValues=&quot;@array/my_color_values&quot; /&gt;</code></pre><p><img src="https://velog.velcdn.com/images/k_hyun/post/32176549-c9ba-4703-b827-8cfefd811ff5/image.png" alt=""></p>
<p>사용자가 입력한 값이나 선택한 값을 summary에 자동으로 지정하려면 SimpleSummaryProvider를 이용한다.</p>
<pre><code class="language-kotlin">val idPreference: EditTextPreference? = findPreference(&quot;id&quot;)
val colorPreference: ListPreference? = findPreference(&quot;color&quot;)

idPreference?.summaryProvider =
    EditTextPreference.SimpleSummaryProvider.getInstance()
colorPreference?.summaryProvider =
    ListPreference.SimpleSummaryProvider.getInstance()
</code></pre>
<p>사용자가 값을 설정하지 않았으면 Not set이라는 문자열이 출력된다.
<img src="https://velog.velcdn.com/images/k_hyun/post/53432c0f-d44d-4875-b5ff-e78db91aa532/image.png" alt="">
값을 지정한 경우 해당 값으로 summary가 변경된다.
<img src="https://velog.velcdn.com/images/k_hyun/post/2ccee6b4-f461-4de3-987b-4aee2950c79d/image.png" alt=""></p>
<pre><code class="language-kotlin">// 이벤트 핸들러 지정
idPreference?.setOnPreferenceClickListener { preference -&gt;
    ...
    true
}    </code></pre>
<h3 id="설정-값-가져오기">설정 값 가져오기</h3>
<p>프리퍼런스를 이용하면설정한 내용이 XML 파일로 저장된다.
설정값을 가져올 때는 PreferenceManager.getDefaultSharedPreferences() 함수를 이용한다.</p>
<p>설정 XML</p>
<pre><code>&lt;EditTextPreference
    app:key=&quot;id&quot;
    app:title=&quot;ID 설정&quot; /&gt;    </code></pre><p>사용자가 입력한 값은 id 키값으로 저장된다.</p>
<pre><code class="language-kotlin">// 설정값 가져오기
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
val id = sharedPreferences.getString(&quot;id&quot;, &quot;&quot;)</code></pre>
<h3 id="설정-변경-순간-감지">설정 변경 순간 감지</h3>
<p>각 프리퍼런스 객체마다 이벤트 핸들러를 직접 지정하여 이벤트를 처리하는 Preference.OnPreferenceChangeListener를 이용하는 방식</p>
<pre><code class="language-kotlin">// 프리퍼런스를 이용한 이벤트 처리
idPreference?.setOnPreferenceChangeListener { preference, newValue -&gt;
    Log.d(TAG, &quot;onCreatePreferences: &quot;)
    true
}</code></pre>
<p>모든 설정 객체의 변경을 하나의 이벤트 핸들러에서 처리하는 SharedPreferences.OnSharedPreferenceChangeListener를 이용하는 방식
register / unregsiter을 해야한다.</p>
<pre><code class="language-kotlin">override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
        Log.d(TAG, &quot;onSharedPreferenceChanged: &quot;)
    }

override fun onResume() {
    super.onResume()
    preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}

override fun onPause() {
    super.onPause()
    preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 파일 보관]]></title>
            <link>https://velog.io/@k_hyun/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%8C%8C%EC%9D%BC-%EB%B3%B4%EA%B4%80</link>
            <guid>https://velog.io/@k_hyun/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%8C%8C%EC%9D%BC-%EB%B3%B4%EA%B4%80</guid>
            <pubDate>Sat, 25 Mar 2023 04:00:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>안드로이드 앱에서 파일을 다룰 때는 대부분 java.io 패키지에서 제공하는 클래스를 이용한다.</p>
</blockquote>
<h4 id="file">File</h4>
<ul>
<li>파일 및 디렉터리를 지칭하는 클래스<h4 id="fileinputstream--fileoutputstream">FileInputStream / FileOutputStream</h4>
</li>
<li>파일에서 바이트 스트림으로 데이터를 일걱나 쓰는 클래스<h4 id="filereader--filewriter">FileReader / FileWriter</h4>
</li>
<li>파일에서 문자열 스트림으로 데이터를 읽거나 쓰는 클래스</li>
</ul>
<h2 id="내장-메모리의-파일-이용">내장 메모리의 파일 이용</h2>
<pre><code class="language-kotlin">// 파일 객체 생성 후 데이터 쓰기
val file = File(filesDir, &quot;test.txt&quot;)
val writeStream: OutputStreamWriter = file.writer()
writeStream.write(&quot;hello world&quot;)
writeStream.flush()

// 파일의 데이터 읽기
val readStream: BufferedReader = file.reader().buffered()
readStream.forEachLine {
    Log.d(TAG, &quot;$it&quot;)
}

// Context 객체로 데이터를 쓰고 읽기
openFileOutput(&quot;test2.txt&quot;, Context.MODE_PRIVATE).use {
    it.write(&quot;hello world!!&quot;.toByteArray())
}
openFileInput(&quot;test2.txt&quot;).bufferedReader().forEachLine {
    Log.d(TAG, &quot;$it&quot;)
}</code></pre>
<p><img src="https://velog.velcdn.com/images/k_hyun/post/43ff3f60-3aa0-46c2-9b44-4b62fe9ae9f5/image.png" alt="">
저장 데이터는 Device Manager -&gt; data -&gt; data -&gt; 패키지명 -&gt; files 에서 확인 가능
<img src="https://velog.velcdn.com/images/k_hyun/post/44ffa25c-d111-456c-a9c7-41d77e630d5d/image.png" alt=""></p>
<h2 id="외장-메모리의-파일-이용하기">외장 메모리의 파일 이용하기</h2>
<p>내부 저장소의 파티션을 나누어 외장 메모리로 제공할 수 있다.</p>
<pre><code class="language-kotlin">// 외장 메모리 사용 가능 여부 판단
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
    Log.d(TAG, &quot;MOUNTED&quot;)
} else {
    Log.d(TAG, &quot;UNMOUNTED&quot;)
}</code></pre>
<p>안드로이드 버전과 API 호환성을 위해 매니페스트에 다음처럼 선언하는 것이 좋다.</p>
<pre><code>&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;/&gt;
    &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/&gt;
    &lt;application
        ...
        android:requestLegacyExternalStorage=&quot;true&quot;&gt;
            ...
        &lt;/application&gt;

&lt;/manifest&gt;</code></pre><p>외장 메모리 공간은 앱별 저장소와 공용 저장소로 구분된다.</p>
<h3 id="앱별-저장소-이용">앱별 저장소 이용</h3>
<p>앱별저장소는 개별 앱에 할당된 공간으로 해당 앱에서만 접근할 수 있다.
앱별 저장소의 파일을 외부 앱에서 접그낳게 하려면 파일 프로바이더로 공개해야 한다.</p>
<pre><code class="language-kotlin">// 앱별 저장소에 파일 쓰고 읽기
val file: File = File(getExternalFilesDir(null), &quot;test.txt&quot;)
val writeStream: OutputStreamWriter = file.writer()
writeStream.write(&quot;hi&quot;)
writeStream.flush()

val readStream: BufferedReader = file.reader().buffered()
readStream.forEachLine {
    Log.d(TAG, &quot;$it&quot;)
}</code></pre>
<p> Device Manager -&gt; storage -&gt; emulated -&gt; 0 -&gt; Android -&gt; data -&gt; 패키지명 -&gt; files 안에 있음
<img src="https://velog.velcdn.com/images/k_hyun/post/67fa0536-1f8d-44bf-ac0b-48a932d105a7/image.png" alt=""></p>
<h3 id="공용-저장소-이용">공용 저장소 이용</h3>
<p>앱에서 만든 파일을 모든 앱이 이용할 수 있게 하고 싶을 때도 있다. 대표적인 사례가 카메라 앱이다.
카메라 앱은 파일을 앱별 저장소가 아닌 공용 저장소에 만든다.
공용 저장소는 모든 앱을 위한 공간이므로 파일을 만든 앱을 삭제해도 파일은 삭제되지 않는다.</p>
<p>공용 저장소는 파일 경로로 직접 접근하지 않고 시스템이 제공하는 API를 이용한다.</p>
<pre><code class="language-kotlin"> // 공용 저장소에 접근
val projection = arrayOf(
         MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME
        )

val cursor = contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        projection,
        null,
        null,
        null
        )

cursor?.let {
    while (cursor.moveToNext()) {
        Log.d(TAG, &quot;_id : ${cursor.getLong(0)}, name : ${cursor.getString(1)}&quot;)
        }
    }

// 이미지 파일의 Uri값 가져오기
val contentUri: Uri = ContentUris.withAppendedId(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(0)
        )

// 이미지 데이터 가져오기
val resolver = applicationContext.contentResolver
resolver.openInputStream(contentUri).use { stream -&gt;
        // stream 객체에서 작업 수행
        val option = BitmapFactory.Options()
        option.inSampleSize = 10
        val bitmap = BitmapFactory.decodeStream(stream, null, option)
        binding.resultImageView.setImageBitmap(bitmap)
    }</code></pre>
<p>contentResolver.query() 함수의 첫 번째 매개변수에 Uri값을 MediaStore.Images를 사용하였다.
이는 안드로이드 이미지 파일의 공용 저장소인 DCIM과 Pictures 디렉터리를 가리킨다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 SQlite]]></title>
            <link>https://velog.io/@k_hyun/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-SQlite</link>
            <guid>https://velog.io/@k_hyun/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-SQlite</guid>
            <pubDate>Fri, 24 Mar 2023 13:06:57 GMT</pubDate>
            <description><![CDATA[<h2 id="sqlite">SQlite</h2>
<blockquote>
<p>안드로이드 폰에서 시용하는 db관리 시스템은 SQLite이다.
앱의 저장소에 파일로 저장하며, 외부 앱에서는 접근할 수 없다.</p>
</blockquote>
<h2 id="질의문-작성">질의문 작성</h2>
<pre><code class="language-kotlin">// DB파일을 열고 객체를 받아온다. 파일이 없으면 새로 만듦
val db = openOrCreateDatabase(&quot;testdb&quot;, Context.MODE_PRIVATE, null)

// 테이블 생성 (create문)
db.execSQL(&quot;create table USER_TB(&quot; + 
    &quot;_id integer primary key autoincrement,&quot; + 
    &quot;name not null,&quot; +
    &quot;phone)&quot;

// 데이터 삽입 (insert문)
db.execSQL(&quot;insert into USER_TB (name, phone) values (?,?)&quot;,
        arrayOf&lt;String&gt;(&quot;name&quot;, &quot;11121212&quot;))

// 데이터 조회 (select문)
val cursor = db.rawQuery(&quot;select * from USER_TB&quot;, null)

// 선택한 행의 값 가져오기
while (cursor.moveToNext()) {
    val name = cursor.getString(0)
    val phone = cursor.getString(1)
}

// insert() 함수 사용
val values = ContentValues()
values.put(&quot;name&quot;, &quot;lee&quot;)
values.put(&quot;phone&quot;, &quot;12345678&quot;)
db.insert(&quot;USER_TB&quot;, null, values)

// query() 함수 사용
val cursor = db.query(&quot;USER_TB&quot;, arrayOf&lt;String&gt;(&quot;name&quot;, &quot;phone&quot;), &quot;phone=?&quot;,
    arrayOf&lt;String&gt;(&quot;12345678&quot;), null, null, null)</code></pre>
<h2 id="데이터베이스-관리하기">데이터베이스 관리하기</h2>
<p>질의문을 실행해야 할 때는 SQLiteDatabase 객체를 이용해야 한다.</p>
<p>추가로 SQLiteOpenHelper 클래스를 이용하면 테이블을 생성, 변경, 제거하는 코드를 작성할 수 있다.</p>
<pre><code class="language-kotlin">class DBHelper(context: Context): SQLiteOpenHelper(context, &quot;testdb&quot;, null, 1) {
    // SQLiteOpenHelper 클래스가 이용되는 순간 한 번 호출한다.
    override fun onCreate(db: SQLiteDatabase?) {}
    // DB 버전 정보가 변경될 때마다 호출한다.
    override fun onUpgrdate(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
} 

// 데이터베이스 객체 생성
val db:SQLiteDatabase = DBHelper(this).writableDatabase</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본 앱과 연동]]></title>
            <link>https://velog.io/@k_hyun/%EA%B8%B0%EB%B3%B8-%EC%95%B1%EA%B3%BC-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@k_hyun/%EA%B8%B0%EB%B3%B8-%EC%95%B1%EA%B3%BC-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Wed, 22 Mar 2023 14:24:37 GMT</pubDate>
            <description><![CDATA[<h2 id="주소록-앱-연동">주소록 앱 연동</h2>
<pre><code>&lt;uses-permission android:name=&quot;android.permission.READ_CONTACTS&quot; /&gt;</code></pre><p>퍼미션을 설정해야 한다.</p>
<pre><code class="language-kotlin">val intent = Intent(ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
requestContactLauncher.launch(intent)</code></pre>
<p>주소록의 목록 화면을 띄우는 코드이다.
주소록 목록 화면은 인텐트를 이용해 실행한다.</p>
<h2 id="갤러리-앱-연동">갤러리 앱 연동</h2>
<p>안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현한다.</p>
<p>BitMapFactory를 이용하면 작은 이미지를 불러오는 데는 문제 없지만 큰 이미지를 불러올 때는 OOM(Out Of Memory)오류가 발생할 수 있다.</p>
<pre><code class="language-kotlin">// 옵션을 지정하지 않고 비트맵 생성
val bitmap = BitmapFactory.decodeStream(inputStream)

// 옵션을 지정해 비트맵 생성
val option = BitmapFactory.Options()
option.inSampleSize = 4
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
</code></pre>
<pre><code class="language-kotlin">// 사진 목록 출력
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = &quot;image/*&quot;
requestGalleryLauncher.launch(intent)</code></pre>
<h2 id="카메라-앱-연동하기">카메라 앱 연동하기</h2>
<p>카메라 앱을 연동하여 결과를 돌려받는 방법은 2가지가 있다.</p>
<p>사진 데이터를 가져오는 방법은 카메라 앱으로 사진을 촬영 후 파일로 저장하지 않고 데이터만 넘겨주는 방식이다. 이 방식은 데이터의 크기가 작다는 단점이 있다.</p>
<p>사진 파일을 공유하는 방법은 사진을 파일에 저장한 후 성공인지 실패인지만 넘겨주는 방식이다. 카메라 성능만큼 큰 크기의 사진을 촬영하고 앱에서 이용할 수 있지만, 준비 작업이 필요하다.</p>
<pre><code class="language-kotlin">// 사진 촬영 액티비티 실행
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
requestCameraTumbnailLauncher.launch(intent)</code></pre>
<p>액션 문자열을 MediaStore.ACTION_IMAGE_CAPTURE로 지정하여 시스템에 전달하면 카메라 앱이 실행된다.</p>
<h3 id="사진-파일을-공유하는-방법">사진 파일을 공유하는 방법</h3>
<p>앱에서 외장 메모리에 파일을 만들어 줘야 한다. 
파일을 만들 때  getExternalStoragePublicDirectory() 또는 getExternalFilesDir() 함수를 이용할 수 있다.
  getExternalStoragePublicDirectory() 함수를 이용하려면 다음의 퍼미션을 설정해 줘야 한다.</p>
<pre><code>  &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot; /&gt;</code></pre><p>  API 레벨 24버전 부터는 엄격 모드가 적용되었다.
  앱끼리 파일을 공유하려면 content:// 프로토콜을 이용하고, URI에 접근할 수 있는 권한을 부여해야 한다.
  이때 FileProvider클래스를 이용한다. FileProvider 클래스는 XML 설정을 기반으로 content:// 프로토콜로 구성된 URI를 생성해 준다.</p>
<p>  res/xml 디렉터리에 파일 프로바이더용 XML 파일을 만들어 줘야 한다.</p>
<pre><code>  &lt;paths xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
      &lt;external-path name=&quot;myfiles&quot; path=&quot;Android/data/패키지명/files/Pictures&quot; /&gt;
  &lt;/paths&gt;</code></pre><pre><code>  &lt;provider&gt;
      ...
      &lt;meta-data
        android:name=&quot;android.support.FILE_PROVIDER_PATHS&quot;
        android:resource=&quot;@xml/file_paths&quot;&gt; &lt;/meta-data&gt;
  &lt;/provider&gt;</code></pre><p>  메타 데이터의 resource속성에 res/xml에 만들어 놓은 XML 파일을 지정한다.</p>
<h2 id="지도-앱-연동하기">지도 앱 연동하기</h2>
<pre><code class="language-kotlin">  val intent = Intent(Intent.ACTION_VIEW, Uri.parse(&quot;geo:37, 126&quot;))
  startActivity(intent)</code></pre>
<p>  지도 앱을 연동할 때는 인텐트 문자열을 Intent.ACTION_VIEW로 지정한다.
  geo:의 숫자는 위도와 경도를 의미한다.</p>
<h2 id="전화-앱-연동하기">전화 앱 연동하기</h2>
<pre><code>  &lt;uses-permission android:name=&quot;android.permission.CALL_PHONE&quot; /&gt;</code></pre><p>  전화를 거는 기능은 위 처럼 퍼미션을 설정해야 한다.</p>
<pre><code class="language-kotlin">  val intent = Intent(Intent.ACTION_CALL, Uri.parse(&quot;tel:02-120&quot;))
  startActivity(intent)</code></pre>
<p>  인텐트를 시스템에 전달하여 전화를 걸 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[콘텐츠 프로바이더]]></title>
            <link>https://velog.io/@k_hyun/%EC%BD%98%ED%85%90%EC%B8%A0-%ED%94%84%EB%A1%9C%EB%B0%94%EC%9D%B4%EB%8D%94</link>
            <guid>https://velog.io/@k_hyun/%EC%BD%98%ED%85%90%EC%B8%A0-%ED%94%84%EB%A1%9C%EB%B0%94%EC%9D%B4%EB%8D%94</guid>
            <pubDate>Wed, 22 Mar 2023 13:57:54 GMT</pubDate>
            <description><![CDATA[<h2 id="콘텐츠-프로바이더">콘텐츠 프로바이더</h2>
<blockquote>
<p>앱끼리 데이터를 연동하는 컴포넌트이다. 
앱을 개발하면서 다른 앱의 데이터를 사용할 때 콘텐츠 프로바이더를 이용한다.</p>
</blockquote>
<h3 id="콘텐츠-프로바이더-작성">콘텐츠 프로바이더 작성</h3>
<pre><code class="language-kotlin">class MyContentProvider : ContentProvider() {

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array&lt;String&gt;?): Int {
        TODO(&quot;Implement this to handle requests to delete one or more rows&quot;)
    }

    override fun getType(uri: Uri): String? {
        TODO(
            &quot;Implement this to handle requests for the MIME type of the data&quot; +
                    &quot;at the given URI&quot;
        )
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO(&quot;Implement this to handle requests to insert a new row.&quot;)
    }

    override fun onCreate(): Boolean {
        TODO(&quot;Implement this to initialize your content provider on startup.&quot;)
    }

    override fun query(
        uri: Uri, projection: Array&lt;String&gt;?, selection: String?,
        selectionArgs: Array&lt;String&gt;?, sortOrder: String?
    ): Cursor? {
        TODO(&quot;Implement this to handle query requests from clients.&quot;)
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array&lt;String&gt;?
    ): Int {
        TODO(&quot;Implement this to handle requests to update one or more rows.&quot;)
    }
}</code></pre>
<p>ContentProvider 클래스를 상속받아서 작성한다.</p>
<p>onCreate(), getType(), query(), insert(), update(), delete() 함수를 재정의해서 작성한다.</p>
<pre><code>&lt;provider
    android:name=&quot;.MyContentProvider&quot;
    android:authorities=&quot;com.example.test_provider&quot;
    android:enabled=&quot;true&quot;
    android:exported=&quot;true&quot;&gt;
&lt;/provider&gt;</code></pre><p>콘텐츠 프로바이더도 안드로이드 컴포넌트이므로 매니페스트에 등록해야 한다.
authorities 속성도 반드시 선언해야 한다.
해당 속성은 외부에서 콘텐츠 프로바이더를 이용할 때 식별값으로 사용되는 문자열이다.</p>
<pre><code>&lt;queries&gt;
    &lt;!-- 둘중 하나만 선언하면 된다. --&gt;
    &lt;!-- &lt;provider android:authorities=&quot;com.example.test_provider&quot; /&gt; --&gt;
    &lt;package android:name=&quot;com.example.name&quot;/&gt;
&lt;/queries&gt;</code></pre><p>외부 앱에서 콘텐츠 프로바이더를 사용하려면 매니페스트에 해당 앱에 관한 패키지 공개 설정을 해줘야 한다.</p>
<pre><code class="language-kotlin">contentResolver.query(
    Uri.parse(&quot;content://com.example.test_provider&quot;),
    null, null, null, null)</code></pre>
<p>시스템에 등록된 콘텐츠 프로바이더를 사용할 때는 ContentResolver 객체를 이용한다.</p>
]]></description>
        </item>
    </channel>
</rss>