<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>‎♡ (っ˘ڡ˘ς) ♡</title>
        <link>https://velog.io/</link>
        <description>신입 개발자👩‍💻</description>
        <lastBuildDate>Thu, 13 May 2021 11:54:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>‎♡ (っ˘ڡ˘ς) ♡</title>
            <url>https://images.velog.io/images/jinny_0422/profile/01878ef5-9bf8-4639-9f00-94cc530ca28f/KakaoTalk_20210323_184554558.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ‎♡ (っ˘ڡ˘ς) ♡. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jinny_0422" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] 방탈출 카페 찾기 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EB%B0%A9%ED%83%88%EC%B6%9C-%EC%B9%B4%ED%8E%98-%EC%B0%BE%EA%B8%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EB%B0%A9%ED%83%88%EC%B6%9C-%EC%B9%B4%ED%8E%98-%EC%B0%BE%EA%B8%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 13 May 2021 11:54:32 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-방탈출-카페-찾기-app">🚩 방탈출 카페 찾기 App</h2>
<p>오늘은 원하는 지역의 방탈출 카페 목록을 지도에 보여주는 App을 만든다!</p>
<p>온라인 예약이 가능한 카페의 경우 &#39;예약하기&#39; 버튼을 통해 예약페이지로 이동할 수 있다.</p>
<p>또한, 카페의 정보를 친구들에게 공유해줄 수 도 있다!<img src="https://images.velog.io/images/jinny_0422/post/3f6bcd65-c7f5-4ea9-adcc-0914f3ef7fb8/GIF%202021-05-24%20%EC%98%A4%ED%9B%84%2011-36-04.gif" alt=""></p>
<h2 id="🚩-사용-기술-및-도구">🚩 사용 기술 및 도구</h2>
<p>언어 : Kotlin (100%)
환경 : Android Studio</p>
<p>사용 기술  </p>
<ul>
<li>Naver Map API</li>
<li>ViewPager2</li>
<li>FrameLayout</li>
<li>CoordinatorLayout</li>
<li>BottomSheetBehavior</li>
<li>Retrofit</li>
<li>Glide</li>
</ul>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<h3 id="🔸-지역명에-따라-방탈출카페-data를-dataclass에-담기">🔸 지역명에 따라 방탈출카페 data를 dataClass에 담기</h3>
<p>API를 통해 데이터 통신을 하기 위해 Retrofit을 사용하였다.
<a href="https://velog.io/@jinny_0422/Retrofit2%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%98%81%ED%99%94-%EC%A0%95%EB%B3%B4%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0">Retrofit 사용법</a>은 지난 포스팅에 정리해놓았다. </p>
<p>◾ 인터페이스 추가 - RoomsSerivece</p>
<pre><code class="language-kotlin">interface RoomsService {
    @GET(api 나머지 주소를 적어주세용)
    fun getRoomsList(
        @Query(&quot;query&quot;) query: String,
        @Query(&quot;displayCount&quot;) displayCount:Int = 20
    ): Call&lt;RoomsResponse&gt;
}</code></pre>
<p>◾ 데이터 클래스 추가 - RoomsModel</p>
<pre><code class="language-kotlin">data class RoomsModel(
    @SerializedName(&quot;id&quot;)
    val id: Int,
    @SerializedName(&quot;name&quot;)
    val title: String,
    @SerializedName(&quot;menuInfo&quot;)
    val price: String,
    @SerializedName(&quot;thumUrl&quot;)
    val imgUrl: String,
    @SerializedName(&quot;x&quot;)
    val lng: Double,
    @SerializedName(&quot;y&quot;)
    val lat: Double,
    @SerializedName(&quot;tel&quot;)
    val tel: String,
    @SerializedName(&quot;abbrAddress&quot;)
    val address: String,
    @SerializedName(&quot;bizhourInfo&quot;)
    val hourInfo: String,
    @SerializedName(&quot;hasNaverBooking&quot;)
    val hasBooking: Boolean,
    @SerializedName(&quot;naverBookingUrl&quot;)
    val bookingUrl: String
) : Serializable
// Serializable을 import 하여 직렬화를 해주어야 한다.</code></pre>
<p>◾ items라는 리스트로 묶었으니 DTO 추가 - data class
내가 받아오는 데이터는 3중으로 구성되어있다.<img src="https://images.velog.io/images/jinny_0422/post/51ed5dbc-983a-4fa9-9a9b-8051e1106e0f/image.png" alt=""></p>
<pre><code class="language-kotlin">data class RoomsResponse(
    @SerializedName(&quot;result&quot;)
    var roomsResult: RoomsResult
)
data class RoomsResult(
    @SerializedName(&quot;place&quot;)
    var place : RoomsList
)
data class RoomsList(
    @SerializedName(&quot;list&quot;)
    // 이렇게 마지막 list에 담긴 아이템들은 RoomsModel 타입의 리스트로 저장한다. 
    val items: List&lt;RoomsModel&gt; = arrayListOf&lt;RoomsModel&gt;()
)</code></pre>
<p>◾ 레트로핏 객체 생성 후 데이터 가져오기</p>
<pre><code class="language-kotlin">// retrofit 구현
val retrofit = Retrofit.Builder().baseUrl(api 기본주소를 적어주세요)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        // retrofit 생성
        retrofit.create(RoomsService::class.java).also {

            // 또한, 바로 생성한 인터페이스의 getRoomsList()를 실행한다
            it.getRoomsList(query = (searchWord.replace(&quot; &quot;,&quot;&quot;)  + &quot; 방탈출카페&quot;))
                .enqueue(object : Callback&lt;RoomsResponse&gt; {
                    override fun onResponse(
                        call: Call&lt;RoomsResponse&gt;,
                        response: Response&lt;RoomsResponse&gt;
                    ) {
                        if (response.isSuccessful.not()) {
                            // 실패 할 경우
                            binding!!.progressbar.visibility = View.INVISIBLE
                            Toast.makeText(
                                this@MainActivity,
                                &quot;에러! 잠시 후 다시 시도해주세요.&quot;,
                                Toast.LENGTH_SHORT
                            ).show()
                            return
                        }
                        // 통신 성공 시
                        response.body()?.let { dto -&gt;
                            binding!!.progressbar.visibility = View.INVISIBLE
                            if (dto.roomsResult.place == null) {
                                binding!!.textField.error = &quot;검색 결과가 없습니다. 또는 지역명을 확인해주세요.&quot;
                                return
                            }
                            // 데이터를 가지고 결과를 보여줄 Activity로 이동
                            val datalist = dto.roomsResult.place.items
                            val intent = Intent(this@MainActivity, ResultActivity::class.java)
                            val list: ArrayList&lt;RoomsModel&gt; = ArrayList&lt;RoomsModel&gt;(datalist)
                            intent.putExtra(&quot;datalist&quot;, list)
                            startActivity(intent)

                        }
                    }

                    override fun onFailure(call: Call&lt;RoomsResponse&gt;, t: Throwable) {
            // 통신 실패 시
                    }
        })
}</code></pre>
<h3 id="🔸-navermap-api-연결하기">🔸 NaverMap API 연결하기</h3>
<p>◾ api 신청 및 clientId 추가하기
api 사용신청은 <a href="https://www.ncloud.com/product/applicationService/maps">네이버 API 사이트</a>에서 신청하면 된다.
신청 완료 후 부여받은 clientId는 따로 values로 저장 후 meta-data에 추가해주어야한다.</p>
<pre><code class="language-kotlin">// api_key.xml
&lt;resources&gt;
    &lt;string name=&quot;naver_map_Client_Id&quot;&gt;부여받은 ClientID&lt;/string&gt;
&lt;/resources&gt;

// Manifest.xml
&lt;meta-data
            android:name=&quot;com.naver.maps.map.CLIENT_ID&quot;
            android:value=&quot;@string/naver_map_Client_Id&quot; /&gt;</code></pre>
<p>◾ 프로젝트에 Naver Map 추가하기</p>
<pre><code class="language-kotlin">// build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        maven(&quot;https://naver.jfrog.io/artifactory/maven/&quot;)
    }
}

dependencies {
    // 네이버 지도 SDK
    implementation(&quot;com.naver.maps:map-sdk:3.11.0&quot;)
}</code></pre>
<p>◾ layout에 mapView 추가 </p>
<pre><code class="language-kotlin">&lt;com.naver.maps.map.MapView
        android:id=&quot;@+id/mapView&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot; /&gt;</code></pre>
<h3 id="🔸-data의-방탈출-카페들을-지도에-마킹하기">🔸 Data의 방탈출 카페들을 지도에 마킹하기</h3>
<p>이제 MainActivity에서 넘겨받은 데이터 list를 가지고 지도에 마킹을 해준다.</p>
<pre><code class="language-kotlin">// 네이버 지도 준비하기
override fun onMapReady(map: NaverMap) {
        naverMap = map

        // 확대/축소 정도 조절!
        naverMap.maxZoom = 18.0
        naverMap.minZoom = 10.0

        val uiSetting = naverMap.uiSettings

        // true로 하면 기본 버튼
        // 현재 위치 버튼
        uiSetting.isLocationButtonEnabled = false
        uiSetting.setLogoMargin(30, 0, 0, 230)
        // 내가 만든 버튼으로 변경
        currentLocationButton.map = naverMap

        locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
        naverMap.locationSource = locationSource

        setListWithdata(datalist)
    }

    //  데이터를 가지고 list를 넘겨주어라!
    private fun setListWithdata(datalist: ArrayList&lt;RoomsModel&gt;) {
        updateMarker(datalist)
        viewPagerAdpater.submitList(datalist)
        recyclerViewAdapter.submitList(datalist)
        bottomSheetTextView.text = &quot;${datalist.size}개의 결과보기&quot;
    }

    // 받은 list 하나씩 지도에 마크 찍기
    private fun updateMarker(rooms: List&lt;RoomsModel&gt;) {
        rooms.forEach { room -&gt;
            val marker = Marker()
            marker.position = LatLng(room.lat, room.lng)
            marker.onClickListener = this
            marker.map = naverMap
            marker.tag = room.id
            marker.icon = MarkerIcons.RED
        }
    }
</code></pre>
<h3 id="🔸-cardview와-지도-마커-연결하기">🔸 CardView와 지도 마커 연결하기</h3>
<pre><code class="language-kotlin">// viewPager(CardView로 생성했음)를 넘길 때 마다
// 해당하는 item의 마커로 지도 이동
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)

                val selectedRoomModel = viewPagerAdpater.currentList[position]
                val cameraUpdate =
                    CameraUpdate.scrollTo(LatLng(selectedRoomModel.lat, selectedRoomModel.lng))
                        .animate(CameraAnimation.Easing)

                naverMap.moveCamera(cameraUpdate)
            }
        })

// 지도 위 마커 클릭 시 해당하는 item의 viewPager로 이동 
override fun onClick(overlay: Overlay): Boolean {
        val selectedModel = viewPagerAdpater.currentList.firstOrNull() {
            it.id == overlay.tag
        }
        selectedModel?.let {
            val position = viewPagerAdpater.currentList.indexOf(it)
            viewPager.currentItem = position
        }
        return true
    }</code></pre>
<h3 id="🔸-장소-정보-보여주기--공유하기-예약하기-">🔸 장소 정보 보여주기 (+ 공유하기, 예약하기 )</h3>
<p>viewPager의 아이템을 클릭 하거나, 하단 뷰의 아이템 클릭 시 
장소 정보 Activity로 이동한다.</p>
<pre><code class="language-kotlin">// 해당 장소의 데이터를 가지고 Activity 이동
private fun goDetailActivity(roomsModel: RoomsModel) {
        val intent = Intent(this, DetailActivity::class.java)
        intent.putExtra(&quot;data&quot;, roomsModel)
        startActivity(intent)
}

// 공유하기 기능을 통해 장소 정보를 외부 앱으로 전달할 수 있음
private fun shareInfo(data: RoomsModel) {
        val intent = Intent().apply {
            action = Intent.ACTION_SEND
            putExtra(
                Intent.EXTRA_TEXT,
                &quot;[방탈출 고고씽!\uD83C\uDFC3] 지금 \&quot;${data.title}\&quot;를 확인해보세요.\n\uD83D\uDC49 https://m.place.naver.com/place/${data.id}  &quot;
            )
            type = &quot;text/plain&quot;
        }
        startActivity(Intent.createChooser(intent, null))
}

// 네이버 예약하기가 가능한 장소의 경우 예약페이지로 이동할 수 있음
private fun goBookingSite(dataUri: String) {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.data = Uri.parse(dataUri)
        startActivity(intent)
}</code></pre>
<h2 id="🚩-완성">🚩 완성!</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/2eff077b-b7d9-4d6a-97ef-d52aade79958/image.png" alt=""></p>
<hr>
<h6 id="관련-문서--naver-map-안드로이드-개발가이드">관련 문서 : <a href="https://navermaps.github.io/android-map-sdk/guide-ko/0.html">NAVER MAP 안드로이드 개발가이드</a></h6>
<h6 id="참고했던-블로그--mechacat님-tistory">참고했던 블로그 : <a href="https://mechacat.tistory.com/15">mechacat님 tistory</a></h6>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 동기들과의 SNS APP만들기]]></title>
            <link>https://velog.io/@jinny_0422/%EB%8F%99%EA%B8%B0%EB%93%A4%EA%B3%BC%EC%9D%98-SNS-APP%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/%EB%8F%99%EA%B8%B0%EB%93%A4%EA%B3%BC%EC%9D%98-SNS-APP%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 01 May 2021 12:27:10 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-동기들끼리-sns">🚩 동기들끼리 SNS</h2>
<p>대학교에 들어가서 꾸준히 활동한 동아리가 있다!
컴퓨터공학과 학술 동아리였는데 임원부터 부회장까지 하면서 개인적으로 애정이 많이 갔다ㅎㅎ
특히 동기들!! 이랑 휴학기간까지 5년동안 진짜 많은 걸 했다.</p>
<p>우리끼리 나중에 우리끼리의 SNS 앱 &amp; 웹을 만들자!! 하는 얘기가 나왔고, 
나는 바로 실행 해버리는 &quot;ENFP&quot; 라서 후딱 만들어봤다. 
(여기까지 나의 TMI 대잔치,,,)</p>
<h2 id="1-사용-기술-및-도구">1. 사용 기술 및 도구</h2>
<ul>
<li>언어 : Kotlin (100%)</li>
<li>환경 : Android Studio</li>
<li>Database : Firebase</li>
<li>그 외 : Glide, Facebook API, Goolge API, View Binding, okhttp, multidex</li>
</ul>
<h2 id="2-작품-설명">2. 작품 설명</h2>
<ul>
<li>사진을 첨부한 게시글 작성</li>
<li>댓글 달기 , 좋아요 누르기</li>
<li>회원가입/로그인/로그아웃 ( 이메일 &amp; Facebook &amp; Google) </li>
<li>프로필 편집 </li>
<li>팔로우 / 언팔로우</li>
<li>알림 기능 (팔로우, 댓글, 좋아요)</li>
<li>사진 다운로드 </li>
</ul>
<h2 id="3-기능-살펴보기">3. 기능 살펴보기</h2>
<h3 id="◾-로그인-및-회원가입">◾ 로그인 및 회원가입</h3>
<p>위 기능들은 예전 다른 프로젝트에서도 여러번 사용해보았기 때문에 어렵지 않게 진행하였다.
방법은 따로 포스팅을 해두었다. 👉 <a href="https://velog.io/@jinny_0422/Android-Kotlin-Android-Kotlin-Firebase%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Facebook-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0">Facebook 로그인</a> &amp; <a href="https://velog.io/@jinny_0422/Android-Kotlin-Firebase%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%A9%94%EC%9D%BC-Google-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0">Google 및 이메일 로그인</a></p>
<hr>
<h3 id="◾-게시글-사진-올리기">◾ 게시글 (+사진) 올리기</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/ed410547-f59c-4272-b12e-02fb3e71edc4/33333333.gif" alt=""></p>
<pre><code class="language-kotlin">// 갤러리로 이동하기
private fun goGallery() {
        val galleryIntent = Intent(Intent.ACTION_PICK) 
        galleryIntent.type = &quot;image/*&quot; //선택한 파일의 종류를 지정해준다 (이미지만 지정하겠다는 뜻)
        intent.putExtra(&quot;crop&quot;, true)
        startActivityForResult(galleryIntent, PICK_IMAGE_FROM_ALBUM)
}

// 이미지 크롭하기
private fun cropImage(uri: Uri?) {
        CropImage.activity(uri).setGuidelines(CropImageView.Guidelines.ON)
            .setCropShape(CropImageView.CropShape.RECTANGLE)
            .start(this)
}

// 사진을 Storage에 업로드
private fun contentUpload() {
        //현재 시간을 String으로 만들기
        //20210306_141238
        val timestamp = SimpleDateFormat(&quot;yyyyMMdd_HHmmss&quot;, Locale.getDefault()).format(Date())
        //만약 나라를 지정하지않고 단말기의 설정된 시간으로 하고 싶다면 Locale.getDefault(), 한국으로 하고 싶으면 Locale.KOREA
        val fileName = &quot;IMAGE_$timestamp.png&quot;

        //서버 스토리지에 접근하기!
        val storageRef = storage?.reference?.child(&quot;images&quot;)?.child(fileName)

        // 서버 스토리지에 파일 업로드하기!
        storageRef?.putFile(photoUri!!)?.continueWithTask() {
            return@continueWithTask storageRef.downloadUrl
            //나중에 이미지를 다른 곳에서 불러오고 할떄 url을 가져올수있게해놓음
        }?.addOnSuccessListener {
            upload(it, fileName)
            Toast.makeText(this, getString(R.string.upload_success), Toast.LENGTH_SHORT).show()
        }?.addOnCanceledListener {
            //업로드 취소 시
        }?.addOnFailureListener {
            //업로드 실패 시
    }
}

// Storage 업로드 된 사진의 uri를 가지고
// Firestore에 게시물 업로드
private fun upload(uri: Uri, fileName: String) {
    //contentDto 객체를 생성한다.
    val contentDto = ContentDto(
              imageUrl = uri.toString(),
              uid = auth?.currentUser?.uid,
              userId = auth?.currentUser?.email,
              explain = contentEditText.text.toString(),
              timestamp = System.currentTimeMillis().toLong(),
              imageStorage = fileName
        )

        //Dto를 Firestore에 추가
        firestore?.collection(&quot;images&quot;)?.document()?.set(contentDto)

        //액티비티 종료
        setResult(Activity.RESULT_OK)
        finish()
}</code></pre>
<hr>
<h3 id="◾-팔로우--언팔로우">◾ 팔로우 / 언팔로우</h3>
<p>팔로워의 경우 Firebase DB를 사용하여 관리 하였다.<img src="https://images.velog.io/images/jinny_0422/post/7e0a78b6-465c-440e-8f9a-93b37c27cafd/2222222.gif" alt="">&#39;Follow&#39; Document에 사용자의 uid로 컬렉션을 두었다.
아래 필드로는 followers와 followings를 두고, 해당되는 사용자의 uid를 저장하였다.<img src="https://images.velog.io/images/jinny_0422/post/95780252-3b00-4bf0-bd75-12c2c2ef7d08/image.png" alt=""> 
이는 Follow Dto 데이터 클래스를 생성하여 리스트를 주고 받았다.</p>
<pre><code class="language-kotlin">data class FollowDto(
    // 이 사람을 팔로잉하는 사람들
    var followers : MutableMap&lt;String, Boolean&gt; = HashMap(),

    // 이 사람이 팔로잉 중인 사람들
    var followings : MutableMap&lt;String, Boolean&gt; = HashMap()
)</code></pre>
<p>실제 팔로우 / 언팔로우의 경우 다음과 같이 구현하였다.
목록에 유저의 uid가 있는지 확인 후 상황에 따라 팔로우/언팔로우를 한다.</p>
<pre><code class="language-kotlin">val firestore = FirebaseFirestore.getInstance()
val currentUid = FirebaseAuth.getInstance().currentUser!!.uid
val txDocTargetUser = firestore?.collection(&quot;follow&quot;)?.document(this.targetUserId!!)
    //Firestore에 데이터 저장 : runTransaction{...}
        firestore.runTransaction {
            // it : Transaction
            // it.get(Document) : 해당 Document 받아오기
            // it.set(Document, Dto 객체) : 해당 Document에 Dto 객체 저장하기
            var followDto = it.get(txDocTargetUser).toObject(FollowDto::class.java)

            if (followDto == null) {
                followDto = FollowDto().apply {
                    followers[currentUid!!] = true
                    notifyFollow()
                }
            } else {
                with(followDto) {
                    if (followers.containsKey(currentUid!!)) {
                        // 언팔로우
                        followers.remove(currentUid!!)
                    } else {
                        // 팔로우
                        followers[currentUid!!] = true
                        notifyFollow()
                    }
                }
            }
    it.set(txDocTargetUser, followDto)
    return@runTransaction
}</code></pre>
<hr>
<h3 id="◾-좋아요-기능">◾ 좋아요 기능</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/1e803998-dd5e-4e4d-aa12-d62088f44ac3/33333333.gif" alt="">좋아요 기능의 경우에도 사실 위의 팔로우 / 언팔로우 기능과 많이 유사하다.
좋아요 한 사용자의 uid를 저장한다. 
유저가 목록에 없다면 &#39;좋아요&#39;, 있다면 &#39;좋아요 취소&#39;를 한다.</p>
<hr>
<h3 id="◾-댓글-달기">◾ 댓글 달기</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/7ed8778d-f749-483f-9405-3b56809625d1/11111111.gif" alt=""></p>
<pre><code class="language-kotlin">// Dto
data class ContentDto(
    // 게시글
    var uid: String? = &quot;&quot;,
    ...
) {
    // 댓글
    // ContentDto.Comment 로 접근한다.
    data class Comment(
        //얘도 primary키 필요함!
        var uid: String? = &quot;&quot;,
        var userId: String? = &quot;&quot;,  // 업로드한 유저의 이메일
        var content: String? = &quot;&quot;, //댓글 내용
        var timeStamp: Long? = null //댓글 작성 시간
    )
}

// 댓글 업로드 함수
private fun commentUpload() {
        val commentDto = ContentDto.Comment(
            uid = auth?.currentUser?.uid,
            userId = auth?.currentUser?.email,
            content = etCommentContent.text.toString(),
            timeStamp = System.currentTimeMillis()
        )
        // 알림 함수 실행
        notifyComment(commentDto.uid, commentDto.userId, commentDto.timeStamp)

        //Dto를 firebase에 추가!
        val doc = firestore?.collection(&quot;images&quot;)?.document(content_uid!!)

        firestore?.runTransaction {
            val contentDto = it.get(doc!!).toObject(ContentDto::class.java)
            doc.collection(&quot;comments&quot;).document().set(commentDto)
            if (contentDto != null) {
                contentDto.commentCount += 1
                it.set(doc, contentDto)
            }
        }
        etCommentContent.setText(&quot;&quot;)
        hideKeyboard()

}</code></pre>
<hr>
<h3 id="◾-사진-다운로드-받기">◾ 사진 다운로드 받기</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/b0ebd11c-e363-4cc4-a61c-3d236d0bdb31/2222222.gif" alt="">downloadManager를 통해 사진을 기기에 다운로드 받는다.
다운로드 알림이 뜨다가 말아서 사알짝 멘붕이였다. 
알고보니 setNotificationVisibility를 설정해주지 않아서 계속 진행되다가 알림창에서 사라지는 것이였다.</p>
<p>또, Directory 경로 설정을 Environment.DIRECTORY_DOWNLOADS 으로 하니
갤러리에서는 확인되지 않는 황당한 경우가 생겼다. 
혹시 PICTURES인가..? 하고 해보니 간단하게 해결 되었다.</p>
<pre><code class="language-kotlin">private fun downloadImage(url: String) {
            val downloadManager: DownloadManager =
                context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

            val request = DownloadManager.Request(Uri.parse(url))
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
            request.setDestinationInExternalPublicDir(
                Environment.DIRECTORY_PICTURES,
                &quot;Aunae&quot; + &quot;.jpg&quot;
            )
            downloadManager.enqueue(request)
}</code></pre>
<hr>
<h3 id="◾-알림-기능">◾ 알림 기능</h3>
<p>알림은 총 3가지 유형이 있다.</p>
<pre><code class="language-kotlin">// Content.kt
const val NOTIFICATION_FAVORITE = 0
const val NOTIFICATION_FOLLOW = 1
const val NOTIFICATION_COMMENT = 2</code></pre>
<p>알림의 정보를 담을 Dto를 생성해준다.</p>
<pre><code class="language-kotlin">data class NotificationDto(
      var destinationUid:String? = &quot;&quot;,
      var sender : String? = &quot;&quot;,
      var senderUid : String? = &quot;&quot;,
      var type : Int? = null,
      var timestamp: Long? = null,
      var timeInfo : String? = &quot;&quot;,
      var contentUid : String? = &quot;&quot;
)</code></pre>
<p>이제 정보를 담아 Firebase에 업로드 해준다.
예시로, 댓글 알림을 업로드 하는 함수이다.</p>
<pre><code class="language-kotlin"> private fun notifyComment(uid: String?, email: String?, timeStamp: Long?) {
        if (uid == destination_uid)
            return
        val date = Date(timeStamp!!)

        // 날짜, 시간을 가져오고 싶은 형태 선언
        val dateFormat = SimpleDateFormat(&quot;yyyy/MM/dd HH:mm:ss&quot;, Locale(&quot;ko&quot;, &quot;KR&quot;))

        // 현재 시간을 dateFormat 에 선언한 형태의 String 으로 변환
        val timeData = dateFormat.format(date)

        val notificationDto = NotificationDto().apply {
            destinationUid = destination_uid
            sender = email
            senderUid = uid
            type = NOTIFICATION_COMMENT
            timestamp = timeStamp
            timeInfo = timeData.toString()
            this.contentUid = content_uid
        }
        FirebaseFirestore.getInstance().collection(&quot;notifications&quot;).document().set(notificationDto)
}</code></pre>
<p>AlarmFragment에서 사용자는 자신에게 온 알림을 확인 할 수 있다.</p>
<pre><code class="language-kotlin">FirebaseFirestore.getInstance().collection(&quot;notifications&quot;)
                .whereEqualTo(&quot;destinationUid&quot;, currentId)
                .addSnapshotListener { value, error -&gt;
                    notifyDtoList.clear()
                    notifyId.clear()
                    if (value == null) return@addSnapshotListener
                    value.forEach {
                        notifyDtoList.add(it.toObject(NotificationDto::class.java))
                        notifyId.add(it.id)
                    }
notifyDataSetChanged()</code></pre>
<p>최대한 기능을 구현한 코드들을 담아보려했는데 너무 길어져서
RecyclerView나 BottomNavigation 등과 같이 다른 포스팅에서도 다룬 적 있는 기능들은 제외해보았다!</p>
<p>만들고 나서 동기들이 사용을 해줄 때 마다 뿌듯함을 느낀다.
웹 버전도 동기들이 빨리 만들어줘서 연동을 해보길 기대해본다!</p>
<hr>
<h5 id="git-보러가기"><a href="https://github.com/hijin315/Aunae16">Git 보러가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 우리동네 미세먼지 APP 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EC%9A%B0%EB%A6%AC%EB%8F%99%EB%84%A4-%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80-APP-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EC%9A%B0%EB%A6%AC%EB%8F%99%EB%84%A4-%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80-APP-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 28 Apr 2021 14:32:11 GMT</pubDate>
            <description><![CDATA[<h1 id="우리동네-미세먼지-app">우리동네 미세먼지 APP</h1>
<p>오늘을 사용자의 위치를 불러와 해당 지역의 실시간 대기 정보를 알려주는 App을 만든다!
크게는 미세먼지와 초미세먼지의 정보를 알려준다.
추가적으로 아황산가스 / 일산화탄소 등 기타 정보들도 알려준다.</p>
<h2 id="🚩-사용-기술-및-도구">🚩 사용 기술 및 도구</h2>
<p>언어 : Kotlin (100%)
환경 : Android Studio</p>
<ul>
<li>Fused Location Provider API</li>
<li>공공데이터 API</li>
<li>카카오 로컬 API</li>
<li>View Binding </li>
<li>okhttp </li>
<li>coroutine</li>
<li>retrofit</li>
</ul>
<h2 id="🚩-작품-설명">🚩 작품 설명</h2>
<p>API를 3가지 이용한다.
(1) Fused Location Provider API
(2) Kakao - 좌표 변환 API
(3) 공공데이터 포털 - 에어코리아 (대기오염정보 , 측정소정보) API</p>
<p>사용자의 위치 정보 불러온 다음 
위 API들을 이용하여 &#39;근접 측정소 정보 불러오기 / 실시간 대기 정보 불러오기&#39; 를 해준다.<img src="https://images.velog.io/images/jinny_0422/post/a22091b2-5d6b-4c11-8aa4-03f1f33f54b3/image.png" alt=""></p>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<h3 id="◾-사용자의-위치-정보-가져오기">◾ 사용자의 위치 정보 가져오기</h3>
<p>사용자의 기기에서 위치 값을 가져오기 위해 &#39;Fused Location Provider API&#39; 를 사용한다.
우선, 두가지 권한이 필요하다!</p>
<pre><code class="language-kotlin">// Manifest.xml
&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.ACCESS_COARSE_LOCATION&quot; /&gt;

// 위치값 가져오기
private var cancellationTokenSource: CancellationTokenSource? = null
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)

cancellationTokenSource = CancellationTokenSource()
fusedLocationProviderClient.getCurrentLocation(
            LocationRequest.PRIORITY_HIGH_ACCURACY,
            cancellationTokenSource!!.token
        ).addOnSuccessListener { location -&gt;
            Log.d(&quot;위치 정보&quot;,&quot;${location.latitude}, ${location.longitude}&quot;)
    }</code></pre>
<h3 id="◾-kakao-api-통해-위치값gps을-tm값으로-변환하기">◾ KAKAO API 통해 위치값(GPS)을 TM값으로 변환하기</h3>
<pre><code class="language-kotlin"> // 코루틴 스콥프
private val scope = MainScope()

scope.launch {
    // 코루틴 시작
    val monitoringStation = Repository.getNearbyCenter(location.latitude, location.longitude)
}</code></pre>
<h3 id="◾-공공데이터---에어코리아로-대기-정보-가져오기">◾ 공공데이터 - 에어코리아로 대기 정보 가져오기</h3>
<h5 id="-아래는-측정소-정보-가져오기-코드입니다-대기-정보-가져오기는-비슷한-코드로-진행되어-생략합니다-자세한건-git에서">(+ 아래는 측정소 정보 가져오기 코드입니다. 대기 정보 가져오기는 비슷한 코드로 진행되어 생략합니다! 자세한건 <a href="https://github.com/hijin315/MyFineDust.git">Git</a>에서)</h5>
<p>api 통신을 통해 받아온 json을 data class로 만들어야한다.
이를 편리하게 해주는 플로그인이 있는데 &#39;JSON to Kolin Class&#39;이다.
json 응답 형식을 붙여넣으면 그걸 토대로 모델을 만들어준다. 
api 를 빠르게 테스트 해볼때 주로 사용한다고 한다.</p>
<p>예시로 받은 json 데이터를 아래와 같이 붙여넣으면 자동으로 data class를 생성해준다.
<img src="https://images.velog.io/images/jinny_0422/post/f4d26d7d-0499-406f-9223-dad59f028212/image.png" alt=""></p>
<ul>
<li>생긴 모습 : <img src="https://images.velog.io/images/jinny_0422/post/f98462ef-55ee-4fd4-bd77-d4ad62093d4f/image.png" alt=""></li>
</ul>
<p>이제 생성된 데이터 클래스를 사용하여 통신을 해보자!</p>
<pre><code class="language-kotlin">object Repository {

    suspend fun getNearbyCenter(latitude: Double, longitude: Double): Station? {
        val tmCoordinates = kakaoLocalAPI.getTmCoordinates(longitude, latitude)
            .body()?.documents
            ?.firstOrNull() // 첫번째 값 없으면 null
        val tmX = tmCoordinates?.x
        val tmY = tmCoordinates?.y
        Log.d(&quot;ttt&quot;, &quot;&quot; + tmX + &quot; &quot; + tmY)
        return airKoreaAPIService
            .getNearbyCenter(tmX!!, tmY!!)
            .body()
            ?.response
            ?.body
            ?.stations
            ?.minByOrNull { // 가장 작은 값 또는 null \
                it.tm ?: Double.MAX_VALUE
            }
    }


    private val airKoreaAPIService: AirKoreaAPI by lazy {
        Retrofit.Builder()
            .baseUrl(Url.AIR_KOREA_API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(buildHttpClient())
            .build()
            .create()
    }

    private fun buildHttpClient(): OkHttpClient =
        OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) {
                // BODY =  다보여줌
                // DEBUG 일때만 다 보여주자!
                HttpLoggingInterceptor.Level.BODY
            } else {
                // NONE = 안보여줌
                HttpLoggingInterceptor.Level.NONE
            }
        }).build()
}</code></pre>
<h3 id="◾-화면에-보여주기">◾ 화면에 보여주기</h3>
<p>이제 대기 정보를 data Class 형식으로 받아왔으니 이를 가져와 layout에 뿌려주면 된다.
추가적으로 화면을 Swipe 하면 새로고침이 되도록 하기 위해 전체 layout을 SwipeRefreshLayout로 감싸주었다.</p>
<pre><code class="language-kotlin">// 데이터를 토대로 화면 구성하기
fun displayAirQualityData(monitoringStation: Station, measuredValue: MeasuredValue) {
        // 뷰 서서히 보여지게
        binding.mainlayout.animate()
            .alpha(1F)
            .start()

        binding.measuringStationName.text = monitoringStation.stationName
        binding.measuringStationAddressTextView.text = &quot;측정소 : ${monitoringStation.addr}&quot;

        (measuredValue.khaiGrade ?: Grade.UNKNOWN).let { grade -&gt;
            binding.root.setBackgroundResource(grade.colorResId)
            binding.totalGradeLabelTextView.text = grade.label
            binding.totalGradleImojiTextView.text = grade.emoji
        }
        with(measuredValue) {
            binding.fineDustInfoTextView.text =
                &quot;미세먼지: $pm10Value ㎍/㎥ ${(pm10Grade ?: Grade.UNKNOWN).emoji}&quot;
            binding.ultraFineDustInfoTextView.text =
                &quot;초미세먼지: $pm25Value ㎍/㎥ ${(pm25Grade ?: Grade.UNKNOWN).emoji}&quot;

            .
            .
            .
            (생략)
        }
}

// 화면 swipe시 새로고침
// SwipeRefreshLayout에 리스너를 달아준다.
binding.refresh.setOnRefreshListener { 
    // 화면 그리기
    fetchAirQuality()
}</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/d7f41776-76a6-4fa0-b1de-e1b0322bb2f7/sdads.gif" alt=""></p>
<hr>
<h5 id="관련-문서--fused-location-provider-api--kakao-developers---좌표계-변환-api--meterial---color-tool">관련 문서 : <a href="https://developers.google.com/location-context/fused-location-provider">Fused Location Provider API</a> &amp; <a href="https://developers.kakao.com/docs/latest/ko/local/dev-guide#trans-coord">kakao developers - 좌표계 변환 api</a> &amp; <a href="https://material.io/resources/color/#!/?view.left=0&amp;view.right=0">Meterial - Color Tool</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 솔로탈출! 매칭 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EC%86%94%EB%A1%9C%ED%83%88%EC%B6%9C-%EB%A7%A4%EC%B9%AD-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EC%86%94%EB%A1%9C%ED%83%88%EC%B6%9C-%EB%A7%A4%EC%B9%AD-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 24 Apr 2021 17:06:21 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-솔로탈출-매칭-app">🚩 솔로탈출! 매칭 App</h2>
<p>오늘은 회원들의 정보를 
카드뷰를 통해 확인하여, 맘에 드는 사람들끼리 매칭을 해주는 app을 만들어본다.
(간단한 기능의 App입니다!)</p>
<h2 id="🚩-사용-기술-및-도구">🚩 사용 기술 및 도구</h2>
<p>언어 : Kotlin (100%)
환경 : Android Studio
Database : Firebase Realtime Database
그 외 : Facebook API, View Binding, Glide, CardView, yuyakaido - CardStackView, ArthurHub-ImageCropper</p>
<h2 id="🚩-작품-기능-설명">🚩 작품 기능 설명</h2>
<ul>
<li>이메일 로그인 / 페이스북 로그인</li>
<li>Opensource Library인 CardStackView 사용으로 보기좋은 애니메이션 구현</li>
<li>회원 정보를 카드뷰를 통해 확인 후, disLike 또는 Like 처리</li>
<li>서로가 Like 한 경우, 매치 리스트 에서 확인 할 수 있다.</li>
</ul>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<h3 id="◾-이메일--페이스북-로그인">◾ 이메일 / 페이스북 로그인</h3>
<p>이 기능들은 예전 포스팅에서 따로 정리를 해두어서 동일한 방법으로 구현하였다.
👉 <a href="https://velog.io/@jinny_0422/Android-Kotlin-Android-Kotlin-Firebase%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Facebook-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0">Facebook 로그인</a> &amp; <a href="https://velog.io/@jinny_0422/Android-Kotlin-Firebase%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%A9%94%EC%9D%BC-Google-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0">이메일 로그인</a>
추가된 점이 있다면, 조금 더 간결해진 코드가 되었다는 것!?</p>
<p>아래 와 같이 새로 가입하는 회원인 경우
메인화면(회원 리스트)을 보여주기 전에, 이름과 프로필 사진을 입력 받는다.<img src="https://images.velog.io/images/jinny_0422/post/55cc3819-4e51-41ef-b164-11f253fe3b2b/3333333333.gif" alt=""></p>
<p>새로 가입하는 회원인지의 유무는 Database에 해당 uid가 존재하는지로 확인했다.</p>
<pre><code class="language-kotlin">userDB = Firebase.database.reference.child(&quot;Users&quot;)
        val currentUserDB = userDB.child(getCurrentUserID())
        currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                if (snapshot.child(&quot;name&quot;).value == null) {
                    showNameInputPopUp()  // 사진과 이름 받기
                    return
                }
                // 유저정보 갱신하기!
                getUnSelectedUsers()
            }

            override fun onCancelled(error: DatabaseError) {
            }
        })</code></pre>
<p>사진을 갤러리에서 불러오고 crop하는 과정은 <a href="https://velog.io/@jinny_0422/Android%EB%B6%88%EB%9F%AC%EC%98%A8ImageCrop%ED%95%98%EA%B8%B0">저번 포스팅</a>에서 정리했었다.</p>
<h3 id="◾-cardstackview-통해-회원-정보-확인-및-likedislike-처리하기">◾ CardStackView 통해 회원 정보 확인 및 Like/DisLike 처리하기</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/c65aaf0b-373f-416a-b470-2fd6422787fb/11111111111.gif" alt="">
회원가입이 완료된 회원이라면 다음과 같이 선택한적 없는 회원의 정보를 가져온다.</p>
<pre><code class="language-kotlin">// getUnSelectedUsers()
userDB.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                if (snapshot.child(&quot;userID&quot;).value != getCurrentUserID()
                    &amp;&amp; snapshot.child(&quot;likeBy&quot;).child(&quot;like&quot;).hasChild(getCurrentUserID()).not()
                    &amp;&amp; snapshot.child(&quot;likeBy&quot;).child(&quot;dislike&quot;)
                        .hasChild(getCurrentUserID()).not()
                ) {
                    val userID = snapshot.child(&quot;userID&quot;).value.toString()
                    var name = snapshot.child(&quot;name&quot;).value.toString()

                    val imageUrl = snapshot.child(&quot;imageUrl&quot;).value.toString()
                    cardItems.add(CardItem(userID, name, imageUrl))
                    adapter.submitList(cardItems)
                    adapter.notifyDataSetChanged()
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                cardItems.find { it.userID == snapshot.key }?.let {
                    it.name = snapshot.child(&quot;name&quot;).value.toString()
                }
                adapter.submitList(cardItems)
                adapter.notifyDataSetChanged()
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {}

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}

            override fun onCancelled(error: DatabaseError) {}
})</code></pre>
<p>카드 넘김 효과는 오픈소스 라이브러리 <a href="https://github.com/yuyakaido/CardStackView">yuyakaido/CardStackView</a>를 사용하여 구현하였다.
Swipe 효과를 줄 수 있어 유용하다. </p>
<p>카드를 왼쪽으로 Swipe 하는 경우 DisLike, 오른쪽의 경우 Like 처리를 한다.</p>
<pre><code class="language-kotlin">override fun onCardSwiped(direction: Direction?) {
        when (direction) {
            Direction.Right -&gt; {
                like()
            }
            Direction.Left -&gt; {
                dislike()
            }
            else -&gt; {
            }
        }
}

// 좋아요를 한 경우
// like()
 private fun like() {
        val card = cardItems[manager.topPosition - 1]
        cardItems.removeFirst() // 옆으로 보내면 카드 없애기 ! 안하면 아래로 쌓임
        userDB.child(card.userID).child(&quot;likeBy&quot;).child(&quot;like&quot;).child(getCurrentUserID())
            .setValue(true)

        saveMatchIfOtherUserLikeME(card.userID) // 서로 좋아한다면 Match리스트에 저장
        Toast.makeText(this, &quot;${card.name}님을 Like 합니다&quot;, Toast.LENGTH_SHORT).show()
}</code></pre>
<h3 id="◾-매치-리스트-확인-하기">◾ 매치 리스트 확인 하기</h3>
<p><img src="https://images.velog.io/images/jinny_0422/post/45f2ab6d-d83f-4c2a-a60e-461e5b6a8a07/image.png" alt="">위 like() 함수가 실행될 때 아래 함수가 실행된다.
그 후, &#39;매치 리스트 보기&#39; 버튼을 눌러 저장된 List를 가져와 보여준다.</p>
<pre><code class="language-kotlin">// 서로 좋아요 했니??
// saveMatchIfOtherUserLikeME()
val otherUserDB = userDB.child(getCurrentUserID()).child(&quot;likeBy&quot;).child(&quot;like&quot;).child(otherUserID)
        // true라면 상대방도 나를 좋아요 한 것!
        otherUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                if (snapshot.value == true) {
                    userDB.child(getCurrentUserID())
                        .child(&quot;likeBy&quot;)
                        .child(&quot;match&quot;)
                        .child(otherUserID)
                        .setValue(true)

                    userDB.child(otherUserID).child(&quot;likeBy&quot;).child(&quot;match&quot;)
                        .child(getCurrentUserID()).setValue(true)

                }
            }

            override fun onCancelled(error: DatabaseError) {
            }
})</code></pre>
<hr>
<h5 id="관련-문서--rthurhub---android-image-cropper--yuyakaido---cardstackview">관련 문서 : <a href="https://github.com/ArthurHub/Android-Image-Cropper">rthurHub - Android Image Cropper</a> &amp;&amp; <a href="https://github.com/yuyakaido/CardStackView">yuyakaido - CardStackView</a></h5>
<h5 id="git-보러가기"><a href="">Git 보러가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 도서 리뷰 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EB%8F%84%EC%84%9C-%EB%A6%AC%EB%B7%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EB%8F%84%EC%84%9C-%EB%A6%AC%EB%B7%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 20 Apr 2021 14:06:40 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-나만의-도서-app">🚩 나만의 도서 App</h2>
<p>오늘은 인터파크 도서의 정보를 통해 다음 기능을 가진 App을 만들어본다. </p>
<blockquote>
<ol>
<li>베스트 셀러 목록을 확인</li>
<li>도서 검색</li>
<li>리뷰 남기기 </li>
</ol>
</blockquote>
<h3 id="사용해-본-기술">사용해 본 기술</h3>
<ul>
<li>Glide</li>
<li>Retrofit2</li>
<li>Room</li>
<li>View Binding</li>
<li>RecyclerView</li>
<li>Open API</li>
</ul>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<ul>
<li>인터파크 Open API를 통해 베스트 셀러 정보 가져오기</li>
<li>인터파크 Open API를 통해 검색 하여 검색 목록 가져오기</li>
<li>Local DB를 이용하여 검색 기록 저장 및 삭제</li>
<li>Local DB를 이용하여 개인 리뷰를 저장</li>
</ul>
<h3 id="🔸-인터파크-open-api-통해-도서-정보-가져오기">🔸 인터파크 Open API 통해 도서 정보 가져오기</h3>
<p>API 통신을 위해 <a href="https://velog.io/@jinny_0422/Retrofit2%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%98%81%ED%99%94-%EC%A0%95%EB%B3%B4%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0">저번 포스팅</a>에서 사용했던 Retrofit을 다시한번 사용해본다. </p>
<ol>
<li>Retrofit2 추가하기<pre><code class="language-kotlin">// dependencies에 다음 두줄 추가
implementation &#39;com.squareup.retrofit2:retrofit:2.9.0&#39;
implementation &#39;com.squareup.retrofit2:converter-gson:2.9.0&#39;
</code></pre>
</li>
</ol>
<p>// 추가로 인터넷 사용 권한도 설정해준다
// Manifest.xml
<uses-permission android:name="android.permission.INTERNET"/></p>
<pre><code>

2. 인터파크 api 키 발급받기
[인터파크 도서 - 마이북피니언](http://book.interpark.com/blog/viewBlogMain.rdo?bid1=w_bgnb&amp;bid2=bwel&amp;bid3=mybookpinion&amp;bid4=001)으로 들어가 &#39;관리&#39;를 누르면![](https://images.velog.io/images/jinny_0422/post/7bc43755-7ee9-4ed9-aafc-b6efd525f930/image.png) 오픈업관리를 눌러 다음과 같이 인증키를 발급받을 수 있다.![](https://images.velog.io/images/jinny_0422/post/49b64737-bad6-48df-8542-c6405f0c26b2/image.png)

포스트맨을 사용하여 데이터가 받아와지는지 확인 해보니 잘 넘어온다!![](https://images.velog.io/images/jinny_0422/post/7cb8bc1e-e3b9-467e-9b72-628ccf732e40/image.png)

3. 데이터 받아오기
- BookService 인터페이스 만들기

```kotlin
interface BookService {
    @GET(&quot;/api/search.api?output=json&quot;)
    fun getBooksByName(
            @Query(&quot;key&quot;) apiKey: String,
            @Query(&quot;query&quot;) keyword: String
    ): Call&lt;SearchBookDto&gt;
    //return형은 Call 반환타입은 SearchBookDto

    @GET(&quot;/api/bestSeller.api?output=json&amp;categotyId=100&quot;)
    fun getBestSellerBooks(
            @Query(&quot;key&quot;) apiKey: String
    ): Call&lt;BestSellerDto&gt;
}</code></pre><ul>
<li>dataclass 만들기
우리가 사용할 데이터는 item의 배열로 들어온다.
<img src="https://images.velog.io/images/jinny_0422/post/171700b4-ab33-424f-b433-28c67257326e/image.png" alt=""><pre><code class="language-kotlin">data class Book(
  // item 배열의 요소 속 id들
  @SerializedName(&quot;itemId&quot;) val id: Long, //itemId요소를 가져와 id라는 변수에 싱크 해준다
  @SerializedName(&quot;title&quot;) val title: String,
  @SerializedName(&quot;description&quot;) val description: String,
  @SerializedName(&quot;coverSmallUrl&quot;) val coverSmallUrl: String
)
</code></pre>
</li>
</ul>
<p>data class SearchBookDto(
    // 제일 처음 데이터 요청 시 title 값과 item 목록을 받아온다
    @SerializedName(&quot;title&quot;) val title: String,
    @SerializedName(&quot;item&quot;) val bookList: List<Book> // Book 형태의 List로 받아온다.
)</p>
<pre><code>- retrofit 구현 및 연결
```kotlin
val retrofit = Retrofit.Builder().baseUrl(&quot;https://book.interpark.com&quot;)
                .addConverterFactory(GsonConverterFactory.create()) //Gson으로 변환
                .build()

bookService = retrofit.create(BookService::class.java)
bookService.getBestSellerBooks(getString(R.string.interpark_api_key))
            .enqueue(object : Callback&lt;BestSellerDto&gt; {
                override fun onResponse(
                    call: Call&lt;BestSellerDto&gt;,
                    response: Response&lt;BestSellerDto&gt;
                ) {
                    // 성공처리
                    if (response.isSuccessful.not()) {
                        return
                    }
                    response.body()?.let {
                        adapter.submitList(it.bookLists)
                    }
                }

                override fun onFailure(call: Call&lt;BestSellerDto&gt;, t: Throwable) {
                    // 실패처리
                    Log.d(TAG, t.toString())
                }
            })
    }</code></pre><h3 id="🔸-local-db로-room-사용하기">🔸 Local DB로 ROOM 사용하기</h3>
<p>ROOM 또한 <a href="https://velog.io/@jinny_0422/Room-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">지난 포스팅</a>에서 다룬 적이 있다.</p>
<ol>
<li><p>Review를 담을 데이터 클래스 구현</p>
<pre><code class="language-kotlin">@Entity
data class Review(
 @PrimaryKey val id: Int?,
 @ColumnInfo(name = &quot;review&quot;) val review: String?
)</code></pre>
</li>
<li><p>Review  Dao 구현</p>
<pre><code class="language-kotlin">@Dao
interface ReviewDao {
 @Query(&quot;SELECT * FROM review WHERE id == :id&quot;)
 fun getOneReview(id: Int): Review

 @Insert(onConflict = OnConflictStrategy.REPLACE) //같은 책의 리뷰가 있으면 새로운 아이로 REPLACE
 fun saveReview(review: Review)
}</code></pre>
</li>
<li><p>RoomDatabase 만들기</p>
<pre><code class="language-kotlin">@Database(entities = [History::class,Review::class],version = 1)
abstract class AppDatabase : RoomDatabase(){
 abstract fun historyDao(): HistoryDao
 abstract fun reviewDao() : ReviewDao
}</code></pre>
</li>
<li><p>Room 구현 및 사용하기</p>
<pre><code class="language-kotlin">private lateinit var db: AppDatabase
db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, &quot;BookSearchDB&quot;)
         .build()
</code></pre>
</li>
</ol>
<p>// 이미 리뷰가 있다면 불러오기
Thread {
    val review = db.reviewDao().getOneReview(model?.id?.toInt() ?: 0)
    runOnUiThread {
        binding.reviewEditText.setText(review?.review.orEmpty())
    }
}.start()</p>
<p>// 리뷰 저장하기 Button 클릭 시
// DB 에 리뷰를 저장
binding.saveButton.setOnClickListener {
            Thread {
                db.reviewDao().saveReview(
                    Review(model?.id?.toInt() ?: 0, binding.reviewEditText.text.toString())
                )
    }.start()
}</p>
<p>```</p>
<h2 id="👩💻-결과">👩‍💻 결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/cb45f417-2fd0-41c1-b412-1253f2a7c9e8/GIF%202021-05-16%20%EC%98%A4%EC%A0%84%202-31-17.gif" alt=""></p>
<hr>
<h5 id="관련-문서--retrofit-공식문서--인터파트-북피니언-api-사용-문서">관련 문서 : <a href="https://square.github.io/retrofit/">Retrofit 공식문서</a> &amp; <a href="http://book.interpark.com/blog/bookpinion/bookpinionOpenAPIInfo.rdo">인터파트 북피니언 api 사용 문서</a></h5>
<h5 id="git-보러가기"><a href="https://github.com/hijin315/MyBooks.git">Git 보러가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 간단한 오늘의 명언 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AA%85%EC%96%B8-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AA%85%EC%96%B8-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 18 Apr 2021 14:30:37 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-오늘의-명언">🚩 오늘의 명언</h2>
<p>오늘은 간단한 App을 만들어 봤다!
명언과 인물을 확인 할 수 있는 간단한 페이지로 구성 되어있다.
페이지는 왼쪽  / 오른쪽으로 무한으로 이동할 수 있다.</p>
<h2 id="🚩-사용기술">🚩 사용기술</h2>
<ul>
<li>Remote Config</li>
<li>ViewPager2</li>
</ul>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<h3 id="◾-remote-config">◾ Remote Config<img src="https://images.velog.io/images/jinny_0422/post/af6efa49-8b8c-4383-bfb1-367ec492e053/image.png" alt=""></h3>
<p>Remote Config(원격 구성)는 Firebase 에서 제공되는 서비스이다.
실제로 코드 수정이나 별도의 배포 없이 앱의 내용을 변경할 수 있는 기능이다. 
아래와 같은 기능을 제공하고 있다.</p>
<blockquote>
<p>1.앱을 업데이트 할때 일정 비율의 사용자에게만 우선 배포하고, 이 기능이 실제로 동작 되어 지는지 확인한다
2.원하는 타이밍에 이미지 / 홍보 문자 등을 표시할 수 있다.
3.AB테스트 : 제한된 테스트 그룹안에서 테스트를 할 수 있다.
4.여러가지 속성값들을 한번에 변경할 수 있다.</p>
</blockquote>
<p>명언 데이터는 Json 형식으로 추가해주었다.<img src="https://images.velog.io/images/jinny_0422/post/cad0e658-11bf-4dc9-8b31-e564b08a759d/image.png" alt=""></p>
<p>데이터는 비동기 형식으로 불러와 사용하였다.</p>
<pre><code class="language-kotlin">val remoteConfig = Firebase.remoteConfig
remoteConfig.setConfigSettingsAsync(
    // 비동기로 세팅
    remoteConfigSettings {
    minimumFetchIntervalInSeconds = 0
    }
    )

remoteConfig.fetchAndActivate().addOnCompleteListener {
    progress.visibility = View.GONE
        if (it.isSuccessful) {
            val quotes = parseQuotesJson(remoteConfig.getString(&quot;quotes&quot;))
                val isNameShow = remoteConfig.getBoolean(&quot;is_name_show&quot;)
                displayQuotes(quotes, isNameShow)
    }
}</code></pre>
<h3 id="◾-viewpager2">◾ ViewPager2</h3>
<p>ViewPager2 - ViewPager 업그레이드 된 버전 
ViewPager2에 어댑터를 추가하여 실제로 랜더링을 하는 부분을 구현한다.</p>
<pre><code class="language-kotlin">// MainActivity.kt
val adapter = QuotePageAdapter(quotes, isNameShow)
viewPager.adapter = adapter
viewPager.setCurrentItem(adapter.itemCount / 2, false)

// QuotePageAdapter.kt
class QuotePageAdapter(private val quotes: List&lt;Quote&gt;, private val isNameShow: Boolean) :
    RecyclerView.Adapter&lt;QuotePageAdapter.QuoteViewHolder&gt;() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = QuoteViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.item_page, parent, false)
    )

    override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
        val actualPosition = position % quotes.size
        holder.bind(quotes[actualPosition], isNameShow)
    }

    override fun getItemCount() = Int.MAX_VALUE

    class QuoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val quoteTextView: TextView = itemView.findViewById(R.id.quoteTextView)
        private val nameTextView: TextView = itemView.findViewById(R.id.nameTextView)

        fun bind(quote: Quote, isNameShow: Boolean) {
            quoteTextView.text = &quot;\&quot;${quote.quote}\&quot;&quot;
            if (isNameShow) {
                nameTextView.text = &quot;- ${quote.name}&quot;
                nameTextView.visibility = View.VISIBLE
            } else {
                nameTextView.visibility = View.GONE
            }
        }
    }
}</code></pre>
<h3 id="◾-pagetransformer">◾ PageTransformer</h3>
<p>PageTransformer는 표시 / 첨부 된 페이지가 스크롤 될 때마다 호출된다. 
애플리케이션이 애니메이션 속성을 사용하여 페이지보기에 사용자 지정 변환을 적용 할 수 있게 한다.</p>
<p>페이지의 위치에 따라 투명도를 조절하여 화면에서 사라지면서 점점 투명해지도록 구현하였다.<img src="https://images.velog.io/images/jinny_0422/post/0362649c-c201-4273-86d9-241088f59924/image.png" alt=""></p>
<pre><code class="language-kotlin">viewPager.setPageTransformer { page, position -&gt;
    when {
                position.absoluteValue &gt;= 1.0F -&gt; {
                    page.alpha = 0F
                }
                position == 0F -&gt; {
                    page.alpha = 1F
                }
                else -&gt; {
                    page.alpha = 1F - 2 * position.absoluteValue
                }
    }
}</code></pre>
<h2 id="🚩-결과">🚩 결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/e8413e3c-6aab-473e-9413-e86285ba738d/tur.gif" alt=""></p>
<hr>
<h5 id="관련-문서--firebase-공식-문서---remote-config--공식-문서---pagetransformer--공식-문서---viewpager2">관련 문서 : <a href="https://firebase.google.com/docs/remote-config">Firebase 공식 문서 - Remote Config</a> &amp;&amp; <a href="https://www.google.com/search?q=pagetransformer&amp;oq=PageTransfo&amp;aqs=chrome.1.69i57j0l2j0i30l7.5462j0j7&amp;sourceid=chrome&amp;ie=UTF-8">공식 문서 - PageTransformer</a> &amp;&amp; <a href="https://developer.android.com/training/animation/vp2-migration?hl=ko">공식 문서 - ViewPager2</a></h5>
<h5 id="git-바로가기"><a href="https://github.com/hijin315/TodayWiseSaying">Git 바로가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Lint found fatal errors while assembling a release target 에러 잡기]]></title>
            <link>https://velog.io/@jinny_0422/Android-Lint-found-fatal-errors-while-assembling-a-release-target-%EC%97%90%EB%9F%AC-%EC%9E%A1%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-Lint-found-fatal-errors-while-assembling-a-release-target-%EC%97%90%EB%9F%AC-%EC%9E%A1%EA%B8%B0</guid>
            <pubDate>Sat, 17 Apr 2021 13:46:51 GMT</pubDate>
            <description><![CDATA[<p>App 개발을 한 후, APK 파일로 빌드 할 때
다음과 같은 오류가 뜨는 경우가 있다.</p>
<blockquote>
<p>Lint found fatal errors while assembling a release target.</p>
</blockquote>
<p><img src="https://images.velog.io/images/jinny_0422/post/ae865dc1-8a4f-46d4-9c82-e44b88a16be3/image.png" alt=""></p>
<p>Clean Project를 하거나, Rebuild Project를 해봐도 해결되지 않는다.
그런데 AVD에서는 APP 실행이 잘되는데 왜 APK 빌드시에만 오류가 날까 ㅠㅠ</p>
<p>문제점을 찾아보니 에러가 발생해서라고 한다. (실행은 잘 돼놓고..!!) 
이를 확인하고 고칠 수 있는 방법이 있다!!<img src="https://images.velog.io/images/jinny_0422/post/a7367d49-2494-4ec9-8277-f0d4696758fd/image.png" alt=""></p>
<p>보기 방식을 Project로 설정 후, app &gt; build &gt; reports 로 들어가
link-results-release-fetal(둘 중 아무파일이나 상관없음!) 파일을 열어보면된다.<img src="https://images.velog.io/images/jinny_0422/post/0ffdccd8-f3a3-40b0-b766-8472a7e4dd68/image.png" alt="">
이런식으로 어느 오류가 어느 위치에서 발생했는지 알 수 있다.
나같은 경우에는 Layout에서 TextView 위젯의 체인을 또 걸어줬다는 이유 였다!!</p>
<p>이 오류들을 잡아주니 잘 빌드된다!<img src="https://images.velog.io/images/jinny_0422/post/9d566bb1-ca6c-4798-880a-d05c90ce3a5f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 녹음기(Recorder) App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EB%85%B9%EC%9D%8C%EA%B8%B0Recorder-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EB%85%B9%EC%9D%8C%EA%B8%B0Recorder-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 16 Apr 2021 15:39:37 GMT</pubDate>
            <description><![CDATA[<h1 id="🚩-녹음기-app">🚩 녹음기 App</h1>
<p>오늘은 음성을 녹음하는 APP을 만들어본다!
사용자는 녹음된 음성을 재생하여 확인 할 수 있고, 초기화를 통해 재녹음을 할 수 있다.
녹음이 잘 되고 있는지 확인 하기 위해 음성의 변화를 그래프로 보여준다.</p>
<h3 id="사용해-본-기술">사용해 본 기술</h3>
<ul>
<li>RECORD AUDIO PERMISSION 체크</li>
<li>Audio Visualizing</li>
<li>MediaRecorder API</li>
<li>Handler</li>
</ul>
<h2 id="🚩-기능-살펴보기">🚩 기능 살펴보기</h2>
<h3 id="◾-mediarecorder-api-사용하여-녹음하기">◾ MediaRecorder API 사용하여 녹음하기</h3>
<ol>
<li>녹음 권한 요청하기<pre><code class="language-kotlin">// Manifest.xml에 추가
&lt;uses-permission android:name=&quot;android.permission.RECORD_AUDIO&quot; /&gt;</code></pre>
</li>
<li>MediaRecorder 객체 생성 및 실행<pre><code class="language-kotlin">// MediaRecorder 생성
recorder = MediaRecorder().apply {
 // 오디오 소스 설정
 setAudioSource(MediaRecorder.AudioSource.MIC)
 // 출력 파일 포맷 설정
 setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
 // 오디오 인코더를 설정
 setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
 // 출력 파일 이름 설정
 setOutputFile(recordingFilePath)
 // 초기화 완료
 prepare()
}
recorder?.start() // 녹음 시작</code></pre>
</li>
</ol>
<h3 id="◾-녹음된-음성-visualizing-하여-보여주기">◾ 녹음된 음성 Visualizing 하여 보여주기</h3>
<ol>
<li>녹음 중인 경우, 음성의 크기를 리스트로 받아온다.<pre><code class="language-kotlin">// VisualizeView.kt
var onRequestCurrentAmplitude: (() -&gt; Int)? = null
</code></pre>
</li>
</ol>
<p>// MainActivity.kt
visualizerView.onRequestCurrentAmplitude = {
    recorder?.maxAmplitude ?: 0
}
visualizerView.startVisualizing(false)</p>
<pre><code>2. 이를 그래프로 보여줄 View를 만들어 시각화한다.
```kotlin
private val amplitudePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = context.getColor(R.color.purple_500)
        strokeWidth = LINE_WIDTH
        strokeCap = Paint.Cap.ROUND // 라인의 양끝을 둥글게 표현
}

// 뷰 그리기
override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

        canvas ?: return

        val centerY = drawingHeight / 2f    // 그래프의 중앙을 센터로 설정
        var offsetX = drawingWidth.toFloat()
        drawingAmplitudes.let {
            if (isReplaying) {
                it.takeLast(replayingPosition) // 
            } else {
                it
            }
        }.forEach { amplitude -&gt;
            val lineLength =
                amplitude / MAX_AMPLITUDE * drawingHeight * 0.8F // 꽉차는 것 보단 조금 여백을 주기 위해 *0.8
            offsetX -= LINE_SPACE       // X축의 어느 부분에 그릴 것인지
            if (offsetX &lt; 0) // 뷰를 벗어난다면!
                return@forEach

            canvas.drawLine(offsetX, centerY - lineLength / 2F, offsetX, centerY + lineLength / 2F, amplitudePaint)
    }
}</code></pre><h3 id="◾-상태에-따라-다른-이미지의-버튼을-보여주기">◾ 상태에 따라 다른 이미지의 버튼을 보여주기</h3>
<p>AppCompatImageButton를 상속받은 클래스 RecordButton.kt</p>
<pre><code class="language-kotlin">class RecordButton(
    context: Context,
    attrs: AttributeSet
) : AppCompatImageButton(context, attrs) {
    fun updateIconWithState(state: State){
        when(state){
            State.BEFORE_RECORDING-&gt;{
                setImageResource(R.drawable.ic_baseline_fiber_manual_record_24)
            }
            State.AFTER_RECORDING -&gt;{
                setImageResource(R.drawable.ic_baseline_play_arrow_24)
            }
            State.ON_PLAYING -&gt; {
                setImageResource(R.drawable.ic_baseline_stop_24)
            }
            State.ON_RECORDING -&gt; {
                setImageResource(R.drawable.ic_baseline_stop_24)
            }
        }
    }
}</code></pre>
<h3 id="◾-녹음시간-타이머-만들기">◾ 녹음시간 타이머 만들기</h3>
<p>AppCompatTextView를 상속받은 클래스 CountUpTextView.kt</p>
<pre><code class="language-kotlin">private var starttimestamp: Long = 0L
    private val countUpAction: Runnable = object : Runnable {
        override fun run() {
            val currentTimeStamp = SystemClock.elapsedRealtime()

            val countTimeSeconds = ((currentTimeStamp - starttimestamp) / 1000L).toInt()
            updateCountTime(countTimeSeconds)
            handler?.postDelayed(this, 1000L) // 1초에 한번 전달
        }
    }

fun startCountUp() {
    starttimestamp = SystemClock.elapsedRealtime()
    handler?.post(countUpAction)
}

fun stopCountUp() {
    handler?.removeCallbacks(countUpAction)
}

fun clearCountUp(){
    updateCountTime(0)
}

private fun updateCountTime(countTimeSeconds: Int) {
        val minutes = countTimeSeconds / 60
        val seconds = countTimeSeconds % 60

        text = &quot;%02d:%02d&quot;.format(minutes, seconds)
}</code></pre>
<h1 id="👩💻-결과">👩‍💻 결과</h1>
<p><img src="https://images.velog.io/images/jinny_0422/post/9167aad5-d0d5-439f-ba5c-185bba3bc82c/GIF%202021-04-29%20%EC%98%A4%EC%A0%84%2012-02-31.gif" alt="">
++ 설치된 모습 <img src="https://images.velog.io/images/jinny_0422/post/dec03ce9-0eef-4df7-aaaf-38fd5011464c/image.png" alt=""></p>
<hr>
<h5 id="관련-문서--안드로이드-공식문서---mediarecorder-개요--안드로이드-공식-문서---visualizer">관련 문서 : <a href="https://developer.android.com/guide/topics/media/mediarecorder">안드로이드 공식문서 - MediaRecorder 개요</a> &amp; <a href="https://developer.android.com/reference/android/media/audiofx/Visualizer">안드로이드 공식 문서 - Visualizer</a></h5>
<h5 id="git-바로가기"><a href="https://github.com/hijin315/SoundRecorder">Git 바로가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Okhttp3 사용하여 네트워크 통신 하기]]></title>
            <link>https://velog.io/@jinny_0422/Android-Okhttp3-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%86%B5%EC%8B%A0-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-Okhttp3-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%86%B5%EC%8B%A0-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 10 Apr 2021 08:36:51 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-okhttp3란">🚩 Okhttp3란?</h2>
<p>OkHttp는 기본적으로 효율적인 HTTP 클라이언트이다.
쉽게 HTTP 기반의 request/response를 할 수 있도록 도와주는 오픈소스 라이브러리이다.
동기, 비동기 방식을 각각 제공하여 개발자가 선택하여 개발할 수 있다.</p>
<h2 id="🚩-설정하기">🚩 설정하기</h2>
<p>우선 인터넷 사용 권한을 추가해준다.</p>
<pre><code class="language-kotlin">//Manifest.xml 에 추가
&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;</code></pre>
<p>그 다음, Okhttp3 Library를 추가해 준다.</p>
<ol>
<li><p>프로젝트명에 우클릭을 한 후, Open Module Settings을 누른다.<img src="https://images.velog.io/images/jinny_0422/post/22a3b144-3bc9-48cc-8e9a-870975c113cc/image.png" alt=""></p>
</li>
<li><p>Project Structure가 열리면 Dependencies → +버튼 → Library Dependency를 눌러준다.<img src="https://images.velog.io/images/jinny_0422/post/5adcb985-26de-4e98-8d7b-f36d97df0785/image.png" alt=""></p>
</li>
<li><p>&#39;okhttp&#39;를 검색한다. (&#39;squareup.okhttp3&#39; 를 선택)<img src="https://images.velog.io/images/jinny_0422/post/99199b31-fbd7-4723-8c65-99e5823d1ac8/image.png" alt=""></p>
</li>
</ol>
<h2 id="🚩-사용하기">🚩 사용하기</h2>
<p>이제 추가한 okhttp3를 사용해보자!
주의할 점은 네트워크 통신 작업은 Thread안에서 실행되어야 한다.</p>
<pre><code class="language-kotlin">// OkHttp를 사용하도록 OkHttpClient 객체를 생성
var client = OkHttpClient()

// client가 요청할 request를 생성
val request: Request = Request.Builder().url(통신할Url).build()

// 요청을 실행한다.
// 동기 처리를 하고자 한다면 execute, 비동기 처리를 원한다면 enqueue를 사용
client.newCall(request).enqueue()

// 요청 결과에 따른 작업을 위해 Callback을 달아줄 수 있다.
//이렇게!
client.newCall(request).enqueue(object : Callback {
            override fun onFailure(request: Request?, e: IOException?) {
                //실패한 경우
            }

            override fun onResponse(response: Response?) {
                //성공한 경우
                println(response?.body()?.string())
            }
        })</code></pre>
<p>추가적으로 해보자면 요청할 작업을 body에 넣고 post/get 작업을 따로 줄 수도 있다.</p>
<pre><code class="language-kotlin">// RequestBody를 따로 만들어준다.
// 예시 ) &quot;name&quot; 항목에 데이터 name 을 넣어라!
val body = FormBody.Builder().add(&quot;name&quot;,name).build() as RequestBody

// request에 만든 body를 post 해준다. (get도 가능)
val request : Request = Request.Builder().url(통신할Url).post(body).build()</code></pre>
<hr>
<h5 id="출처--square---okhttp-문서">출처 : <a href="https://square.github.io/okhttp/">square - OKHttp 문서</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 뽀모도로 타이머 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 08 Apr 2021 13:03:50 GMT</pubDate>
            <description><![CDATA[<p>오늘은 뽀모도로 타이머를 만들어봤다.</p>
<blockquote>
<p>뽀모도로는 기법은 시간 관리 방법론 25분간 집중해서 일을 한 다음 5분간 휴식하는 방식이다. &#39;뽀모도로&#39;는 이탈리아어로 토마토를 뜻한다. 
프란체스코 시릴로가 대학생 시절 토마토 모양으로 생긴 요리용 타이머를 이용해 25분간 집중 후 휴식하는 일처리 방법을 제안한데서 그 이름이 유래했다. <a href="https://ko.wikipedia.org/wiki/%ED%8F%AC%EB%AA%A8%EB%8F%84%EB%A1%9C_%EA%B8%B0%EB%B2%95">출처 - 위키백과</a></p>
</blockquote>
<h2 id="🚩-뽀모도로-타이머">🚩 뽀모도로 타이머</h2>
<p>사용자가 SeekBar를 이용하여 원하는 시간을 분 단위로 맞춘다.
시간을 설정하면 째깍 거리는 소리와 함께 카운트다운이 실행된다.
카운트 다운 완료 후에는 00초를 알리는 알람을 울려준다.</p>
<h3 id="사용해-본-기술">사용해 본 기술</h3>
<ul>
<li>SeekBar</li>
<li>SoundPool</li>
</ul>
<h2 id="🚩-배운점">🚩 배운점</h2>
<h3 id="1-seekbar">1. SeekBar</h3>
<p>seekBar의 타이머가 카운트다운 되고 있을 때, 시간을 재설정하면 
시간이 두가지로 나뉘어 함께 카운트 되는 이슈가 있었다.
SeekBar의 onStopTrackingTouch(), 즉 시간 설정이 끝났을 때 타이머를 재시작하는 로직으로 구현하여 타이머가 하나 더 생성된 것이 이유였다. 
시간을 변경 하려고 seekBar를 터치하는 순간에 타이머를 정지 시키기 위해, onStartTrackingTouch() 에 타이머를 정지 시키도록 구현하여 해결 하였다.</p>
<h3 id="2-soundpool">2. SoundPool</h3>
<pre><code class="language-kotlin">// soundPool 빌드 처리
private val soundPool = SoundPool.Builder().build()
// 알람
private var bellId: Int? = null
bellId = soundPool.load(this, R.raw.timer_bell, 1)

bellId?.let { it -&gt;
    soundPool.play(it, 1F, 1F, 0, -1, 1F)
}</code></pre>
<p>soundPool.play에 필요한 파라미터들은 다음과 같다.<img src="https://images.velog.io/images/jinny_0422/post/74f8a217-2db4-4bf3-9051-48358108f215/image.png" alt="">추가적으로, soundPool의 경우 메모리를 많이 잡아 먹기 때문에 destroy될 때 마다 할당된 음악을 해제해주는 것이 좋다.</p>
<pre><code class="language-kotlin"> override fun onDestroy() {
        super.onDestroy()
        soundPool.release()  // soundPool에 로드되었던 파일들 해제
}</code></pre>
<h3 id="3-app의-window-색-변경">3. App의 window 색 변경</h3>
<p>UI 전체 색을 변경하더라도, APP을 시작할때 잠깐, 하얀색이 비춰지는 것을 볼 수 있다.
APP의 UI도 결국, window창 위에 그려지는 것이기 때문에 이 window창 색 까지 변경 해 주어야 하는 것이다.</p>
<pre><code class="language-kotlin">&lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;
    &lt;style name=&quot;Theme.PomodoroTimer&quot; parent=&quot;Theme.MaterialComponents.DayNight.NoActionBar&quot;&gt;
        &lt;!-- 이 부분을 추가하여 변경해준다. --&gt;
        &lt;item name=&quot;android:windowBackground&quot;&gt;@color/main_red&lt;/item&gt;
    &lt;/style&gt;
&lt;/resources&gt;</code></pre>
<h2 id="👩💻-결과">👩‍💻 결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/703bba4e-1c7d-4049-9d3c-1b5e49f8865d/GIF%202021-04-25%20%EC%98%A4%EC%A0%84%202-06-00.gif" alt="">Gif로 첨부해야해서 아쉽다!!
실제로 영상을 보면 째깍째깍 하는 소리가 넘 상큼하다.</p>
<ul>
<li>++ 앱 아이콘도 귀엽게 만들어 주었다 ㅎㅎ
<img src="https://images.velog.io/images/jinny_0422/post/b8f9e371-f86e-4ff9-8f22-50b9af59007b/image.png" alt=""></li>
</ul>
<hr>
<h5 id="git-보러가기"><a href="https://github.com/hijin315/Pomodoro_Timer.git">Git 보러가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Entry name 'classes.dex' collided 에러 잡기]]></title>
            <link>https://velog.io/@jinny_0422/Android-Entry-name-classes.dex-collided-%EC%98%A4%EB%A5%98-%EC%9E%A1%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-Entry-name-classes.dex-collided-%EC%98%A4%EB%A5%98-%EC%9E%A1%EA%B8%B0</guid>
            <pubDate>Tue, 06 Apr 2021 14:47:53 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@jinny_0422/Android-multidex-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">지난 포스팅</a>에서 dex 에 대해서 알아봤다!
이제 이 dex로 인한 오류 &#39;Entry name &#39;classes.dex&#39; collided&#39;를 알아본다.<img src="https://images.velog.io/images/jinny_0422/post/c85ea41b-5c83-440c-9d32-8809895e6ff9/image.png" alt="">
65535 개의 메서드가 넘어가는 경우 컴파일된 dex파일이 많아져 위와 같은 오류가 난다.</p>
<h2 id="🚩-buildgradle-app-에-dex-설정-추가하기">🚩 build.gradle (:app) 에 dex 설정 추가하기</h2>
<pre><code class="language-kotlin">android {
    defaultConfig {
        // 이 줄을 추가해준다.
        multiDexEnabled true
}

// minSdkVersion 20 이하인 경우에는 다음 줄도 추가해준다.
dependencies {
    implementation &#39;androidx.multidex:multidex:2.0.1&#39;
}</code></pre>
<h2 id="🚩-manifestxml-설정하기">🚩 Manifest.xml 설정하기</h2>
<pre><code class="language-kotlin">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.example.test&quot;&gt;
    &lt;application
            android:name=&quot;android.support.multidex.MultiDexApplication&quot;&gt;
        &lt;/application&gt;
&lt;/manifest&gt;</code></pre>
<p>여기까지 잘 설정해준다면 잘 빌드되는 것을 확인할 수 있다.<img src="https://images.velog.io/images/jinny_0422/post/1ec23eee-da54-43fc-948b-3763784af12f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] multidex 라이브러리 사용하기
]]></title>
            <link>https://velog.io/@jinny_0422/Android-multidex-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-multidex-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 05 Apr 2021 11:19:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/jinny_0422/post/eb10cfb1-85fb-4598-b737-8c97cca8845d/image.png" alt=""></p>
<h2 id="🚩-multidex란">🚩 multidex란?</h2>
<p>몇일전 App하나를 개발하고 있는데 이러한 오류가 떴다.</p>
<pre><code class="language-java">Cannot fit requested classes in a single dex file...</code></pre>
<p>에러메세지를 가지고 구글링을 하면서 &#39;mutidex&#39; 라이브러리를 알게되었다.
<img src="https://images.velog.io/images/jinny_0422/post/41c3e71d-60e5-4fb1-b6e3-eb97c528185f/image.png" alt=""></p>
<blockquote>
<p>안드로이드 소스코드는 dex파일로 컴파일 되어진다. 
이때, 65535 개의 메서드만 컴파일이 가능하며 이보다 많아지는 경우 컴파일이 불가하다</p>
</blockquote>
<p>이를 초과하는 앱을 만들 때는 이 dex 파일을 쪼개주는 multidex 라이브러리를 사용해주면 된다.</p>
<h2 id="🚩-설정하기">🚩 설정하기</h2>
<p>설정 및 사용 방법은 아주 간단하다!</p>
<p>알아보니 minSdkVersion가 21 (API 21) 이상이라면 기본적으로 사용 설정이 되어있기 때문에 MultiDex를 설정해 줄 필요가 없다.</p>
<p>20 이하라면 아래와 같이 따로 설정을 해주어야 한다.</p>
<p>1) build.gradle (app)에 추가</p>
<pre><code class="language-kotlin">android {
    defaultConfig {
        multiDexEnabled true
    }
}

dependencies {
  implementation &#39;com.android.support:multidex:1.0.3&#39;
}</code></pre>
<p>2) Manifest.xml에 추가</p>
<pre><code class="language-kotlin">// application단 안
android:name=&quot;android.support.multidex.MultiDexApplication&quot; &gt;</code></pre>
<h2 id="🚩-사용하기">🚩 사용하기</h2>
<p>그 후, 문제의 Class 안에 다음과 같이 선언해주면 된다.</p>
<pre><code class="language-kotlin">class MainActivity : MultiDexApplication() {...}</code></pre>
<hr>
<h5 id="썸네일-출처--이미지-출처"><a href="http://imamfarisi.com/mengatasi-error-multidex-android/">썸네일 출처</a> &amp;&amp; <a href="https://parkho79.tistory.com/101">이미지 출처</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Jetpack - LiveData 사용하기]]></title>
            <link>https://velog.io/@jinny_0422/Jetpack-LiveData-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Jetpack-LiveData-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 04 Apr 2021 07:41:33 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@jinny_0422/View-model-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%B4%EC%A1%B4%ED%95%98%EA%B8%B0">지난 포스팅</a>에서 만든 Todo App에 Live Data를 사용해보겠다!!</p>
<h2 id="🚩-livedata란">🚩 LiveData란?</h2>
<p>LiveData는 관찰가능한 데이터 Holder 클래스로, Android Jetpack의 구성요소이다.
지난 포스팅에 사용한 Viewmodel은 이러한 LiveData를 가지고 있다.</p>
<p><img src="https://images.velog.io/images/jinny_0422/post/6f5f57f4-bc1f-434e-b3be-3172c14bff55/image.png" alt="">
예를 들어 위 그림과 같이 음식점 메뉴를 나타내는 ViewModel 안에 LiveData가 있다고 가정하자!
LiveData의 &#39;가격&#39;이 변경되면 UI에서 알아채고, 변경된 데이터를 처리할 수 있다. 
안드로이드 3.1 이상부터 LiveData 및 Viewmodel은 데이터 바인딩과 함께 작동한다.</p>
<p>Activity, Fragment 등의 LifeCycle을 인식하여 LifeCycle 내에서만 동작하는 요소로 LifeCycle이 종료되면(화면 destroy) 같이 삭제된다.
그러므로 메모리 누수가 없고, 수명주기 문제를 해결해주어 데이터 관리를 개발자가 하지 않아도 된다는 점 등 많은 이점을 가지고 있다.</p>
<blockquote>
<p>LiveData는 옵저버 패턴 관련 즉, 데이터의 변경 사항을 알 수 있다.</p>
</blockquote>
<h2 id="🚩-사용하기">🚩 사용하기</h2>
<p>🔸 build.gradle - dependencies 에 추가</p>
<pre><code>def lifecycle_version = &quot;2.2.0
// viewModel
implementation &quot;androidx.lifecycle:lifrcycle-viewmodel-ktx:$lifecycle_version&quot;
// LiveData
implementation &quot;androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version&quot;</code></pre><p>🔸 생성 및 사용하기</p>
<pre><code>// ViewModel 생성
val myViewModel = ViewModelProviders.of(this).get(MyDataViewModel::class.java)
val binding = ActivityMainBindind.inflate(layoutInflater)

// 생성한 ViewModel과 Databinding을 연결한 후
binding.viewmodel = myViewModle

// 다음 코드를 추가해준다.
binding.setLifecycleOwner(this)
setContentView(binding.root)</code></pre><h3 id="🚩-전체-코드">🚩 전체 코드</h3>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel : TodoAdapter.MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        binding.rvItem.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = TodoAdapter(emptyList(), onClickDelete = {
                viewModel.deleteTodo(it)
            }, onClickItem = {
                viewModel.toggleTodo(it)
            })
        }

        binding.addBtn.setOnClickListener {
            val todo = Todo(binding.addEt.text.toString())
            viewModel.addTodo(todo)
        }

        //관찰하여 UI를 업데이트 하도록 observe() 구현
        viewModel.todoLiveData.observe(this, Observer {
            (binding.rvItem.adapter as TodoAdapter).setData(it)
        })
    }
}

data class Todo(
    val text: String,
    var isDone: Boolean = false
) 

class TodoAdapter(
    private var MyDataset: List&lt;Todo&gt;,
    val onClickDelete: (todo: Todo) -&gt; Unit,
    val onClickItem: (todo: Todo) -&gt; Unit
) :
    RecyclerView.Adapter&lt;TodoAdapter.TodoListHolder&gt;() {
    class TodoListHolder(val binding: ItemTodoBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoListHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false)
        return TodoListHolder(ItemTodoBinding.bind(view))
    }

    override fun getItemCount(): Int = MyDataset.size

    // 이 부분 추가!
    fun setData(newData : List&lt;Todo&gt;){
        MyDataset = newData
        notifyDataSetChanged()
    }

    override fun onBindViewHolder(holder: TodoListHolder, position: Int) {
        val todo = MyDataset[position]
        holder.binding.tvList.text = todo.text
        if (todo.isDone) {
            holder.binding.tvList.apply {
                paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
                setTypeface(null, Typeface.ITALIC)
            }
        } else {
            holder.binding.tvList.apply {
                paintFlags = 0
                setTypeface(null, Typeface.NORMAL)
            }
        }
        holder.binding.ivDelete.setOnClickListener { onClickDelete.invoke(todo) }
        holder.binding.root.setOnClickListener { onClickItem.invoke(todo) }
    }

    class MainViewModel : ViewModel() {
    // LivaData로 하면 읽기만 가능하니 수정이 가능한 MutableLiveData를 쓴다.
        val todoLiveData = MutableLiveData&lt;List&lt;Todo&gt;&gt;()

        private val data = arrayListOf&lt;Todo&gt;()

        fun toggleTodo(todo: Todo) {
            todo.isDone = !todo.isDone
            // 추가!
            todoLiveData.value = data
        }

        fun addTodo(todo: Todo) {
            data.add(todo)
            // 추가!
            // todoLivaData의 값을 변경된 data로 갱신해준다.
            todoLiveData.value = data
        }

        fun deleteTodo(todo: Todo) {
            data.remove(todo)
            // 추가!
            todoLiveData.value = data
        }
    }
}</code></pre>
<hr>
<h5 id="출처--안드로이드-공식문서--썸네일-출처">출처 : <a href="https://developer.android.com/reference/android/arch/lifecycle/LiveData">안드로이드 공식문서</a> &amp;&amp; <a href="https://devvkkid.tistory.com/141">썸네일 출처</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] View model 사용하여 데이터 보존하기]]></title>
            <link>https://velog.io/@jinny_0422/View-model-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%B4%EC%A1%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/View-model-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%B4%EC%A1%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 02 Apr 2021 20:50:55 GMT</pubDate>
            <description><![CDATA[<p>오늘은 ViewModel을 사용하여 데이터를 유지하는 간단한 todoList 앱을 만들어 본다.</p>
<h2 id="🚩-viewmodel이란">🚩 ViewModel이란?</h2>
<p>ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되어있다. </p>
<blockquote>
<p>ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있다.</p>
</blockquote>
<p>일반적인 방법으로 데이터를 관리 시 다음과 같이
기기의 회전 처리를 하면 데이터가 날아가는걸 확인 할 수 있다.<img src="https://images.velog.io/images/jinny_0422/post/ed8dcc95-5210-438c-a77b-9be1383164a8/r2r2r2r2r2r.gif" alt=""></p>
<p>화면 회전 시 activity가 destroy후 create가 되기 때문이다.
이를 해결하기 위해 구글에서 제공해주는 View model을 사용해본다.</p>
<h3 id="🔸-viewmodel-추가하기">🔸 viewModel 추가하기</h3>
<p>viewmodels() 사용을 위해 android-ktx를 추가해준다.</p>
<pre><code class="language-kotlin">//build.gradle - dependencies에 추가해준다.
implementation &quot;androidx.fragment:fragment-ktx:1.3.2&quot; </code></pre>
<h3 id="🔸-viewmodel-사용하기">🔸 viewmodel 사용하기</h3>
<p>(1) ViewModel 객체 생성</p>
<pre><code class="language-kotlin">class MyViewModel : ViewModel() {
    private val users: MutableLiveData&lt;List&lt;User&gt;&gt; by lazy {
        MutableLiveData().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData&lt;List&lt;User&gt;&gt; {
        return users
    }

    private fun loadUsers() {

    }
}</code></pre>
<p>(2) Activity에서 사용되도록 설정하기</p>
<pre><code class="language-kotlin">class MyActivity : AppCompatActivity() {

private val viewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        // 위 MyViewModel을 가져와 쓰겠다.
        val model: MyViewModel by viewModels()

        // 위 데이터를 observe(관찰)하겠다.
        model.getUsers().observe(this, Observer&lt;List&lt;User&gt;&gt;{ users -&gt;
            // 업데이트 할 작업
        })
    }
}</code></pre>
<p>크게는 위와 같이 설정하여 사용이 가능하다.
아래는 ViewModel을 사용한 TodoList 앱이다.</p>
<h3 id="🚩-전체코드">🚩 전체코드</h3>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    //ViewModel 객체 선언
    private val viewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        binding.rvItem.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = TodoAdapter(viewModel.data, onClickDelete = {
                //viewModel의 deleteTodo를 실행
                viewModel.deleteTodo(it)
                binding.rvItem.adapter?.notifyDataSetChanged()
            }, onClickItem = {
                viewModel.toggleTodo(it)
                binding.rvItem.adapter?.notifyDataSetChanged()
            })
        }

        binding.addBtn.setOnClickListener {
            val todo = Todo(binding.addEt.text.toString())
            viewModel.addTodo(todo)
            binding.rvItem.adapter?.notifyDataSetChanged()
        }
    }
}

data class Todo(
    val text: String,
    var isDone: Boolean = false
) //자동으로 게터세터가 생성된 dataclass ~!~!


class TodoAdapter(
    private var myDataset: List&lt;Todo&gt;,
    val onClickDelete: (todo: Todo) -&gt; Unit,
    val onClickItem: (todo: Todo) -&gt; Unit
) :
    RecyclerView.Adapter&lt;TodoAdapter.TodoViewHolder&gt;() {
    class TodoViewHolder(val binding: ItemTodoBinding) : RecyclerView.ViewHolder(binding.root)
    //.root 하면 본인이 어떤 뷰로 부터 생성된 바인딩인지 확인 가능

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false)
        return TodoViewHolder(ItemTodoBinding.bind(view))
    }

    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        val todo = myDataset[position]
        holder.binding.tvList.text = todo.text
        if (todo.isDone) {
            holder.binding.tvList.apply {
                paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
                setTypeface(null, Typeface.ITALIC)
            }
        } else {
            holder.binding.tvList.apply {
                paintFlags = 0
                setTypeface(null, Typeface.NORMAL)
            }
        }
        holder.binding.ivDelete.setOnClickListener {
            onClickDelete.invoke(todo)
        }
        holder.binding.root.setOnClickListener {
            onClickItem.invoke(todo) //todo를 전달하여 onClickItem함수를 실행시킨다.
        }
    }

    override fun getItemCount(): Int = myDataset.size

}

class MainViewModel : ViewModel() {
    val data = arrayListOf&lt;Todo&gt;()
    fun toggleTodo(todo: Todo) {
        todo.isDone = !todo.isDone
    }

    fun addTodo(todo: Todo) {
        data.add(todo)
    }

    fun deleteTodo(todo: Todo) {
        data.remove(todo)
    }
}</code></pre>
<h2 id="👩💻-결과">👩‍💻 결과</h2>
<p>화면 회전시에도 UI의 데이터가 남아있는것을 확인 할 수 있다.
<img src="https://images.velog.io/images/jinny_0422/post/5863002c-a923-46ff-94fa-d7c9bac0dc80/rrrrrrr.gif" alt=""></p>
<hr>
<h5 id="출처--안드로이드-공식문서---viewmodel">출처 : <a href="https://developer.android.com/topic/libraries/architecture/viewmodel?hl=en">안드로이드 공식문서 - ViewModel</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] APP에 필요한 권한 사용자에게 얻기]]></title>
            <link>https://velog.io/@jinny_0422/APP%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B6%8C%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%97%90%EA%B2%8C-%EC%8A%B9%EC%9D%B8-%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/APP%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B6%8C%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%97%90%EA%B2%8C-%EC%8A%B9%EC%9D%B8-%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Fri, 02 Apr 2021 14:47:31 GMT</pubDate>
            <description><![CDATA[<p>APP을 사용하다 보면 사용자에게 권한을 승인받아야 할 때가 있다.</p>
<blockquote>
<p>지도 app을 만들어 사용자의 위치를 받아오거나, 사진을 찍기 위해 카메라 접근 권한을 받아올 때 등등</p>
</blockquote>
<p>오늘은 필요한 권한을 추가하고, 사용자에게 승인을 받고, 승인을 받았는지 여부를 확인 하는 작업을 해본다.</p>
<h2 id="1-필요-권한을-manifest에-추가해준다">1. 필요 권한을 Manifest에 추가해준다.</h2>
<pre><code class="language-kotlin">&lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.CAMERA&quot;/&gt;
&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot;
</code></pre>
<h2 id="2-권한이-있는지-확인-및-요청하기">2. 권한이 있는지 확인 및 요청하기</h2>
<pre><code class="language-kotlin">
val cameraPermissionCheck = ContextCompat.checkSelfPermission(this,android.Manifest.permission.CAMERA)
    if(cameraPermissionCheck != PackageManager.PERMISSION_GRANTED){
        // 권한이 없는 경우
            // 사용자에게 권한 승인 요청을 한다.
            ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA),1000)
         } else{
                // 권한이 있는 경우
                Log.d(&quot;permissions&quot;,&quot;권한이 이미 있음&quot;)
     }
}</code></pre>
<h2 id="3-사용자-응답에-따른-작업-추가하기">3. 사용자 응답에 따른 작업 추가하기</h2>
<p>onRequestPermissionsResult()를 ovveride하여 사용자의 응답에 따라 할 작업을 추가할 수 있다.</p>
<pre><code class="language-kotlin">override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array&lt;out String&gt;,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if(requestCode == 1000){
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                // 승낙된 경우 할 작업
                Log.d(&quot;permissions&quot; , &quot;승낙되었습니다.&quot;)
            }else {
                // 거부된 경우 할 작업
                Log.d(&quot;permissions&quot; , &quot;거부되었습니다.&quot;)
            }
        }
    }</code></pre>
<h2 id="👩💻-결과">👩‍💻 결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/7c861c66-18cd-4b1d-9212-a10e4dbeb7e5/dsa.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] AsyncTask 알아보기 & 실습]]></title>
            <link>https://velog.io/@jinny_0422/Android-AsyncTask-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@jinny_0422/Android-AsyncTask-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Wed, 31 Mar 2021 08:11:00 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-async란">🚩 Async란?</h2>
<p>Sync는 동기, Async는 비동기를 뜻한다.</p>
<h3 id="🔸-동기sync-방식">🔸 동기(Sync) 방식</h3>
<ul>
<li>작업을 순서대로 진행한다.</li>
<li>코드의 윗부분부터 아랫부분까지 실행되어진다.
ex. A -&gt; B -&gt; C -&gt; D</li>
</ul>
<h3 id="🔸-비동기async-방식">🔸 비동기(Async) 방식</h3>
<ul>
<li>Thread를 만들어서 작업을 따로 처리한다.</li>
</ul>
<h2 id="🚩-async의-장점과-단점">🚩 Async의 장점과 단점</h2>
<ul>
<li><p>장점</p>
<ul>
<li>MainThread를 기다리게 할 필요가 없다.<ul>
<li>네트워크 작업에 매우 유용하다.</li>
</ul>
</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>재사용이 불가능하다.</li>
<li>구현된 액티비가 종료될 때 따라 종료되지 않는다. 
( Lifecycle을 활요하여( ex. onPause ) 별도로 종료 처리를 해주어야 함 )</li>
<li>AsyncTask는 하나만 실행 될 수 있다. (병렬 처리 불가)</li>
</ul>
</li>
</ul>
<h2 id="🚩-android에서-async를-다루는-방법">🚩 Android에서 Async를 다루는 방법</h2>
<ul>
<li>AsyncTask를 상속받는다.</li>
<li>필요 함수들을 Implement 한다.<ul>
<li>onPreExecute() : Thread가 출발하기 전 실행할 작업</li>
<li>doInBackground() : Thread 진행 시 해야할 작업</li>
<li>onProgressUpdate() : 진행되어 상태가 update 될 때</li>
<li>onPostExecute() : doInBackground가 끝났을 때 (작업이 완료) 실행된다.</li>
</ul>
</li>
</ul>
<h2 id="🚩-asynctask-실습해보기">🚩 AsyncTask 실습해보기</h2>
<ol>
<li><p>AsyncTask 클래스를 만들어준다.</p>
<pre><code class="language-kotlin">class BackgroundAsyncTask(
 val progressBar: ProgressBar, val progressTextView: TextView
) : AsyncTask&lt;Int, Int, Int&gt;() {
 var percent: Int = 0
 override fun onPreExecute() {
     percent = 0
     progressBar.setProgress(percent)
 }
 override fun doInBackground(vararg params: Int?): Int {
     while (isCancelled() == false){
         percent++
         if(percent&gt;100) break
         else publishProgress(percent)
         try {
             Thread.sleep(100)
         }catch (e : Exception){
             e.printStackTrace()
         }
     }
     return percent
 }

 override fun onProgressUpdate(vararg values: Int?) {
     progressBar.setProgress(values[0] ?: 0)
     progressTextView.setText(&quot;퍼센트 : &quot; + values[0])
     super.onProgressUpdate(*values)
 }

 override fun onPostExecute(result: Int?) {
     progressTextView.setText(&quot;작업이 완료 되었습니다.&quot;)
 }

 override fun onCancelled() {
     progressBar.setProgress(0)
     progressTextView.setText(&quot;작업이 취소 되었습니다.&quot;)
 }
}</code></pre>
</li>
<li><p>View에서 버튼 클릭 시 사용되도록 설정한다.</p>
<pre><code class="language-kotlin">asyncStartButton = findViewById(R.id.async_start_btn)
asyncStopButton = findViewById(R.id.async_stop_btn)
</code></pre>
</li>
</ol>
<p>var task : BackgroundAsyncTask? = null</p>
<p>asyncStartButton.setOnClickListener {
    task = BackgroundAsyncTask(asyncProgressBar,asyncTextView)
            task?.execute() // task 실행
}
asyncStopButton.setOnClickListener {
    task?.cancel(true) // task 종료
}</p>
<p>```</p>
<h1 id="👩💻-결과">👩‍💻 결과</h1>
<p><img src="https://images.velog.io/images/jinny_0422/post/6965cb9b-b985-4282-9b0c-e9929c16d707/fififififi.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] '얼마나 돌려?' App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/%EC%96%BC%EB%A7%88%EB%82%98-%EB%8F%8C%EB%A0%A4-App</link>
            <guid>https://velog.io/@jinny_0422/%EC%96%BC%EB%A7%88%EB%82%98-%EB%8F%8C%EB%A0%A4-App</guid>
            <pubDate>Mon, 29 Mar 2021 12:59:30 GMT</pubDate>
            <description><![CDATA[<p>요 몇일 밤낮이 바뀌어 간편 조리 식품을 자주 먹었다.
피자를 전자레인지를 돌리다가 갑자기 아이디어가 번뜩여서 만들어 봤다.
그 날 갑자기 식탁에 있던 종이에 빠르고 짧게 기획을 했던...ㅋㅋ<img src="https://images.velog.io/images/jinny_0422/post/80aab7e3-b80c-4145-805c-e4972e9fc31c/image.png" alt=""></p>
<h2 id="🚩-1-작품-개요">🚩 1. 작품 개요</h2>
<ul>
<li>작품명 : &#39;얼마나 돌려?&#39;</li>
<li>개발기간 : 3일</li>
</ul>
<h2 id="🚩-2-사용-기술-및-도구">🚩 2. 사용 기술 및 도구</h2>
<ul>
<li>언어 : Kotlin (100%)</li>
<li>환경 : Android Studio</li>
<li>Database : Realm &amp; Firebase-FireStorage</li>
</ul>
<h2 id="🚩-3-작품-설명">🚩 3. 작품 설명</h2>
<p>3개의 BottomNavigation View를 가진다.</p>
<h3 id="🔸-homefragment">🔸 HomeFragment</h3>
<p>: 타이머가 있는 뷰이다. 타이머 하단의 AppCompatSeekBar를 이용하여 시간을 조절 할 수 있다.</p>
<pre><code class="language-kotlin">lateinit var seekBar: AppCompatSeekBar
companion object { lateinit var r2: Ringtone }

// &#39;시작&#39; 버튼 클릭 시
seekBar.visibility = View.INVISIBLE
minute = timeTick / 60
second = timeTick % 60

timer(period = 1000, initialDelay = 1000) {
    activity?.runOnUiThread { timer.text = String.format(&quot;%02d : %02d&quot;, minute, second)}
        if (second == 0) {
                if(minute == 0){
                    try {
                                r2.play()
                            } catch (e: Exception) {
                                e.printStackTrace()
                            }
                            cancel()
                        }
                        minute--
                        second = 60
                    }
                    second--
                    btn_stop.setOnClickListener {
                        seekbarSetUp()
                        cancel()
                    }
       }
}

// &#39;정지&#39; 버튼 클릭 시
r2.stop()
seekBar.setProgress(0)
timeTick = 0
seekBar.visibility = View.VISIBLE</code></pre>
<h3 id="🔸-itemfragment">🔸 ItemFragment</h3>
<p>: 제품에 대한 정보는 FirebaseFireStore에 저장해 놨다.<img src="https://images.velog.io/images/jinny_0422/post/c7c0feaa-3db9-4327-b05b-6b1d3735f826/image.png" alt="">이 정보들을 가져와 RecyclerView에 뿌려준다.
&#39;즐겨찾기&#39; 버튼을 누르면 기기 내부DB에 저장되도록 Realm을 사용하였다.</p>
<pre><code class="language-kotlin">data class ItemDto(
    var uid:String? = &quot;&quot;,
    var itemName:String?= &quot;&quot;,  // 제품이름
    var itemTime:String? = &quot;&quot;,  // 전자렌지 시간
    var itemOption:String? = &quot;&quot; //제품 옵션
)

// 제품의 데이터를 ItemDto에 담아 아래 함수를 실행한다.
fun itemLike(itemDto: ItemDto) {
    Realm.init(context)
      val config: RealmConfiguration = RealmConfiguration.Builder()
              .deleteRealmIfMigrationNeeded()
              .build()

    Realm.setDefaultConfiguration(config)
    val realm = Realm.getDefaultInstance()
    realm.executeTransaction{
              with(it.createObject(ItemRealm::class.java)){
                  this.itemTime_r = itemDto.itemTime
                  this.itemOption_r = itemDto.itemOption
                  this.itemName_r = itemDto.itemName
               }
    }
}</code></pre>
<blockquote>
<p><strong>즉, 전체 item 정보는 FirebaseFirestore , 즐겨찾기 목록은 Realm을 사용하여 데이터를 관리했다.</strong></p>
</blockquote>
<p>검색 기능도 구현해놨다!</p>
<pre><code class="language-kotlin">fun search(serachWord: String) {
            firestore?.collection(&quot;item&quot;)
                ?.addSnapshotListener { querySnapshot, firebaseFirestoreException -&gt;
                    // ArrayList 비워줌
                    itemDtoList.clear()

                    for (snapshot in querySnapshot!!.documents) {
                        if (snapshot.getString(&quot;itemName&quot;)!!.contains(serachWord)) {
                            var item = snapshot.toObject(ItemDto::class.java)
                            itemDtoList.add(item!!)
                        }
                    }
                    notifyDataSetChanged()
       }
}</code></pre>
<h3 id="🔸-infofragment">🔸 InfoFragment</h3>
<p>: 화면 하단에 Google Admob을 사용하여 배너 광고를 삽입했다. 
&#39;문의하기 / 제품 추가 신청하기&#39; 를 통해 접수된 정보가 있으면 FirebaseFireStore에 저장된다.</p>
<p>두 레이아웃 모두 이메일 입력창이 있는데 TextWatcher를 사용하여 이메일 형식이 맞는지 계속 확인한다. 
이 부분은 <a href="https://velog.io/@jinny_0422/Android-EditText%EC%9D%98-%EC%9D%B4%EB%A9%94%EC%9D%BC-%ED%98%95%EC%8B%9D-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0">따로 포스팅</a>이 되어있다.</p>
<h2 id="4-느낀-점">4. 느낀 점</h2>
<p>Thread에 대해서 공부하면서 알게된 runOnUiThread를 사용해 볼 기회가 생겨서 좋았다. 몰랐다면 왜 TextView의 값이 변경되지 않는지 방법을 찾아 헤맸을 것 같은 느낌...!</p>
<p>또한, 때에 따라 기기 내부 DB와 외부 DB를 함께 사용해보며 데이터 관리를 해본 점이 인상 깊었다.</p>
<p>한번 만들어 볼까?? 하고 시작하여 구글 스토어에 업로드 하기까지
뼈대를 점점 갖추고, 하나 둘 필요한 살점들을 붙여나가면서 조립하는 듯한 재미를 느꼈다.</p>
<h5 id="-210407-google-playstore-출시">+++ (21.04.07) <a href="https://play.google.com/store/apps/details?id=com.jinny.howlong">Google playstore 출시</a></h5>
<h5 id="-210403-version-12-update--다크-모드-추가">+++ (21.04.03) Version 1.2 Update : 다크 모드 추가</h5>
<h5 id="-210408-version-20-update--타이머-처리-방식-비동기로-변경">+++ (21.04.08) Version 2.0 Update : 타이머 처리 방식 비동기로 변경</h5>
<h5 id="-210409-version-22-update--타이머-중첩-에러-확인-및-수정">+++ (21.04.09) Version 2.2 Update : 타이머 중첩 에러 확인 및 수정</h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] ViewBinding에 대해서 간단히 알아보기]]></title>
            <link>https://velog.io/@jinny_0422/ViewBinding%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/ViewBinding%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 22 Mar 2021 08:00:48 GMT</pubDate>
            <description><![CDATA[<h2 id="🚩-viewbinding이란">🚩 ViewBinding이란?</h2>
<p>binding 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있다. 
모듈에서 사용 설정된 ViewBinding은 모듈에 있는 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 
binding Class의 인스턴스에는 상응하는 레이아웃에 ID가 있는 모든 뷰의 직접 참조가 포함된다.</p>
<p>쉽게 말하자면 xml 레이아웃의 아이템들을 findViewById 보다 간편하게 가져와 사용할 수 있다.
대부분의 경우 ViewBinding이 findViewById를 대체한다.</p>
<h3 id="🔸-findviewbyid와-차이점">🔸 findViewById와 차이점!</h3>
<ul>
<li><p>Null safety: ViewBinding은 뷰에 대한 직접 참조를 생성하기 때문에 View ID가 잘못됨으로 인한 NPE를 발생시킬 위험이 없다. </p>
</li>
<li><p>Type safety: 각 바인딩 클래스의 필드에는 XML 파일에서 참조하는 뷰와 일치하는 타입을 갖고 있어, 클래스를 캐스팅할 때 발생할수 있는 오류가 없다.</p>
</li>
</ul>
<p>레이아웃 파일과 실제 자바/코틀린 코드간에 일치하지 않아 발생하는 문제가 런타임이 아닌 컴파일타임에 발생하기 때문에 더 빠르게 에러를 잡고, 개발자는생산성을 늘릴 수 있습니다.</p>
<h2 id="🚩-사용하기">🚩 사용하기</h2>
<ol>
<li><p>build.gradle (모듈) 에 추가한다.</p>
<pre><code class="language-kotlin">android {
     ...
     viewBinding {
         enabled = true
     }
 }</code></pre>
</li>
<li><p>Class 내에서 사용 설정하기</p>
<pre><code class="language-kotlin">private lateinit var binding: ActivityMainBinding
// activity_main.xml = ActivityMainBinding

 override fun onCreate(savedInstanceState: Bundle) {
     super.onCreate(savedInstanceState)
     binding = ResultProfileBinding.inflate(layoutInflater)
     val view = binding.root
     setContentView(view)
 }</code></pre>
</li>
</ol>
<blockquote>
<p>참고로!
class TodoListHolder(val binding: ItemTodoBinding) : RecyclerView.ViewHolder(binding.root)
와 같이 .root 를 통해 본인이 어떤 뷰로 부터 생성된 바인딩인지 확인 가능하다.</p>
</blockquote>
<ol start="3">
<li>실제 사용하기<pre><code class="language-kotlin"></code></pre>
</li>
</ol>
<p>class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
    // View Binding 사용 순서 1
    private var _binding : ActivityMainBinding? = null
    val binding get()= _binding!!</p>
<pre><code>override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // View Binding 사용 순서 2
    _binding = ActivityMainBinding.inflate(layoutInflater)
    // View Binding 사용 순서 3
    // bottomNavigationView = findViewById(R.id.nav_bottom)
    // bottomNavigationView.setOnNavigationItemSelectedListener(this)
    binding.navBottom.setOnNavigationItemSelectedListener(this)


}
override fun onDestroy() {
    super.onDestroy()
    // 구현순서 4
    // 추가적으로!! Activity가 destroy 될 때 binding객체도 없애야 좋다.
    _binding = null
}</code></pre><p>```</p>
<hr>
<h5 id="참고--view-binding-관련-블로그--view-binding-공식문서">참고 : <a href="https://charlezz.medium.com/view-binding-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-df3526d909a7">view Binding 관련 블로그</a> &amp; <a href="https://developer.android.com/topic/libraries/view-binding">view-binding 공식문서</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 로또 번호 생성기 App 만들기]]></title>
            <link>https://velog.io/@jinny_0422/Android-%EB%A1%9C%EB%98%90-%EB%B2%88%ED%98%B8-%EC%83%9D%EC%84%B1%EA%B8%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-%EB%A1%9C%EB%98%90-%EB%B2%88%ED%98%B8-%EC%83%9D%EC%84%B1%EA%B8%B0-App-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 20 Mar 2021 02:42:12 GMT</pubDate>
            <description><![CDATA[<p>오늘은 로또 번호 생성기 App을 만들어 본다! 단일 Activity로 이뤄져있다.</p>
<h2 id="🚩-로또-번호-생성기">🚩 로또 번호 생성기</h2>
<p>사용자가 들어갔으면 하는 번호를 선택 할 수 있다.
나머지 번호들은 자동 생성기능을 통해 생성해 낼 수 있다.
로또 숫자별로 색이 다른 점을 이용해 ui를 귀엽게 꾸며봤다.</p>
<h2 id="🚩-사용해-본-kotlin-문법">🚩 사용해 본 kotlin 문법</h2>
<ul>
<li>apply</li>
<li>When</li>
<li>Random</li>
<li>Collection - Set, List</li>
<li>lambda</li>
</ul>
<h2 id="🚩-배운-점">🚩 배운 점</h2>
<p>Collection을 통해 숫자가 중복되지 않는 list를 만드는 방법을 배웠다.
Random 함수가 코틀린에서 어떻게 쓰이는지 사용해 볼 수 있었다.
list에 중복되지 않는 숫자 6개를 랜덤으로 넣어야한다.
나는 3가지 방법으로 구현해보았다.</p>
<ol>
<li>첫번째 방법<pre><code class="language-kotlin">val random = java.util.Random()
val list = mutableListOf&lt;Int&gt;()
</code></pre>
</li>
</ol>
<p>while(list.size&lt;6){
    val randomNum = random.nextInt(45)+1
       if(list.contains(randomNum)){
            continue
        }
    list.add(randomNum)
}</p>
<pre><code>
2. 두번째 방법
```kotlin
val random = java.util.Random()
val numberSet = mutableSetOf&lt;Int&gt;()

while(numberSet.size&lt;6){
    val randomNum = random.nextInt(45)+1
    numberSet.add(randomNum)
}</code></pre><ol start="3">
<li>세번째 방법<pre><code class="language-kotlin">val random = java.util.Random()
val list = mutableListOf&lt;Int&gt;().apply{
 for(i in 1..45){
         this.add(i)
 }
}
list.shuffle()
list =  list.subList(0,6))</code></pre>
</li>
</ol>
<h2 id="👩💻-결과">👩‍💻 결과</h2>
<p><img src="https://images.velog.io/images/jinny_0422/post/ddd317b1-3f7b-4c6c-80ec-a237fad4fd37/sssss.gif" alt=""></p>
<hr>
<h5 id="git-바로가기"><a href="https://github.com/hijin315/LottoNum.git">Git 바로가기</a></h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] PHP와 MYSQL을 이용한 DB 관리하기 + 닷홈 웹호스팅 (1)]]></title>
            <link>https://velog.io/@jinny_0422/Android-PHP%EC%99%80-MYSQL%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-DB-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jinny_0422/Android-PHP%EC%99%80-MYSQL%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-DB-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 17 Mar 2021 13:26:17 GMT</pubDate>
            <description><![CDATA[<p>오늘을 재빠르게 방법을 순서대로 설명하겠습니다.</p>
<h2 id="🚩-닷홈에서-웹-호스팅-신청하여-도메인-만들기">🚩 닷홈에서 웹 호스팅 신청하여 도메인 만들기</h2>
<ol>
<li><p>닷홈에서 계정을 생성한다.</p>
</li>
<li><p>로그인 후, 웹호스팅 &gt; 무료호스팅으로 들어간다.<img src="https://images.velog.io/images/jinny_0422/post/8414e308-26d6-474f-b70c-b394e7bb8894/image.png" alt=""></p>
</li>
<li><p>웹 호스팅 설정 정보를 입력해준다.
여기서 FTP ID/PW 와 DB명/ID/PW는 까먹지 않게 따로 메모해두는 것이 좋다. 추후 안드로이드 스튜디오 연결 시 필요한 정보들이다.<img src="https://images.velog.io/images/jinny_0422/post/839d969b-9a44-485b-8203-6d12c080d5a9/image.png" alt=""></p>
</li>
<li><p>나머지 정보들도 확인 후 동의 / 신청하기를 눌러준다. 모두 완료가 되면 웹호스팅 목록에 도메인이 생성된 것을 확인 할 수 있다.<img src="https://images.velog.io/images/jinny_0422/post/531c7426-03c4-4cd6-86ae-88330704db64/image.png" alt=""> </p>
</li>
</ol>
<h2 id="🚩-filezilla로-php-파일-업로드하기">🚩 FileZilla로 php 파일 업로드하기</h2>
<p>fileZilla는 대용량 파일 전송에 적합한 오픈 소스 기반의 프로그램이다. FTP 클라이언트를 이용해서 원격에 파일을 업로드하고, 다운로드 할 수 있다. <a href="https://filezilla.softonic.kr/download">다운로드 하러가기!</a>
아래는 다운로드 후 실행한 화면이다. </p>
<ol>
<li><p><strong>호스트 = 도메인주소 / 사용자명 = FTP ID / 비밀번호 = FTP PW / 포트 = 21</strong> 을 적어준 후 &#39;빠른연결&#39;을 누른다.<img src="https://images.velog.io/images/jinny_0422/post/a126cea7-6228-4ec1-91a5-3c438db3a7c6/image.png" alt=""></p>
</li>
<li><p>연결이 성공하면 아래와 같이 디렉터리 목록이 조회되는데 html 폴더로 들어간다. 이곳에 PHP 파일을 올릴 것이다.<img src="https://images.velog.io/images/jinny_0422/post/8c5b19a7-eb23-46e8-9d62-e16cd8f15ff0/image.png" alt=""></p>
</li>
</ol>
<h2 id="🚩-database-생성하기">🚩 Database 생성하기</h2>
<ol>
<li><p>다시 닷홈으로 들어가 &#39;마이 닷홈 &gt; 호스팅 관리 &gt; 제공 내역(최하단에 위치)&#39;으로 들어간다. 
여기서 MySQL관리 주소로 들어간다.<img src="https://images.velog.io/images/jinny_0422/post/34c40555-9c37-4f03-ab7f-dab83e48e6e7/image.png" alt="">들어가서 로그인을 해주면 된다.(FTP 계정과 동일)<img src="https://images.velog.io/images/jinny_0422/post/1d497291-3bc7-414a-937b-272c06bc5f69/image.png" alt=""></p>
</li>
<li><p>로그인 완료 후 상단의 SQL을 눌러 쿼리문을 작성해준다.
담고자 하는 데이터 테이블을 생성하면 된다.<img src="https://images.velog.io/images/jinny_0422/post/f10b1d7f-b8db-4019-9d20-210808970b19/image.png" alt="">예를 들어 친구들의 정보를 담는다고 가정하고 FRIENDS 테이블과 2개의 칼럼을 만든다.
만든 후에는 실행을 눌러 저장하면 된다.</p>
<pre><code>CREATE TABLE FRIENDS (
     Name VARCHAR(50) NOT NULL,
         StudentID VARCHAR(50) NOT NULL,
         CONSTRAINT PRIMARY KEY(StudentID)
);</code></pre><p><img src="https://images.velog.io/images/jinny_0422/post/4c0d0908-cefe-458f-815c-ab3f60fc6ef9/image.png" alt="">만들기 성공!</p>
</li>
</ol>
<h2 id="🚩-php-파일-추가하기">🚩 PHP 파일 추가하기</h2>
<p>정보를 넣는 php 파일들을 만든다. 
++ 참고로 아직 php 지식이 조금 부족해서 구글링을 통해 코드를 가져와봤다. 
&lt;insertdata.php&gt;</p>
<pre><code>&lt;?php 

    error_reporting(E_ALL); 
    ini_set(&#39;display_errors&#39;,1); 

    include(&#39;start.php&#39;);


    $android = strpos($_SERVER[&#39;HTTP_USER_AGENT&#39;], &quot;Android&quot;);


    if( (($_SERVER[&#39;REQUEST_METHOD&#39;] == &#39;POST&#39;) &amp;&amp; isset($_POST[&#39;submit&#39;])) || $android )
    {

        $StudentID = $_POST[&#39;StudentID&#39;];
        $Name    = $_POST[&#39;Name&#39;];

        if(empty($StudentID)){
            $errMSG = &quot;학번을 입력하세요.&quot;;
        }
        else if(empty($Name)){
            $errMSG = &quot;이름을 입력하세요.&quot;;
        }

        if(!isset($errMSG)) // 이름과 학번 모두 입력
        {
            try{
                // SQL문을 실행하여 데이터를 MySQL 서버의 FRIENDS 테이블에 저장
                $stmt = $con-&gt;prepare(&#39;INSERT INTO FRIENDS(Name, StudentID) VALUES(:Name, :StudentID)&#39;);
                $stmt-&gt;bindParam(&#39;:Name&#39;, $Name);
            $stmt-&gt;bindParam(&#39;:StudentID&#39;, $StudentID);

                if($stmt-&gt;execute())
                {
                    $successMSG = &quot;새로운 친구를 추가했습니다.&quot;;
                }
                else
                {
                    $errMSG = &quot;친구 추가 에러&quot;;
                }

            } catch(PDOException $e) {
                die(&quot;Database error: &quot; . $e-&gt;getMessage()); 
            }
        }

    }

?&gt;


&lt;?php 
    if (isset($errMSG)) echo $errMSG;
    if (isset($successMSG)) echo $successMSG;

    $android = strpos($_SERVER[&#39;HTTP_USER_AGENT&#39;], &quot;Android&quot;);

    if( !$android )
    {
?&gt;
    &lt;html&gt;
       &lt;body&gt;

            &lt;form action=&quot;&lt;?php $_PHP_SELF ?&gt;&quot; method=&quot;POST&quot;&gt;
                Name: &lt;input type = &quot;text&quot; name = &quot;Name&quot; /&gt;
                StudentID   : &lt;input type = &quot;text&quot; name = &quot;StudentID&quot; /&gt;
                &lt;input type = &quot;submit&quot; name = &quot;submit&quot; /&gt;
            &lt;/form&gt;

       &lt;/body&gt;
    &lt;/html&gt;

&lt;?php 
    }
?&gt;</code></pre><p>&lt;start.php&gt;</p>
<pre><code>&lt;?php

    $host = &#39;localhost&#39;;
    $username = &#39;jinny0422&#39;; # MySQL 계정 아이디
    $password = &#39;wlsdl316!&#39;; # MySQL 계정 패스워드
    $dbname = &#39;jinny0422&#39;;  # DATABASE 이름


    $options = array(PDO::MYSQL_ATTR_INIT_COMMAND =&gt; &#39;SET NAMES utf8&#39;);

    try {

        $con = new PDO(&quot;mysql:host={$host};dbname={$dbname};charset=utf8&quot;,$username, $password);
    } catch(PDOException $e) {

        die(&quot;Failed to connect to the database: &quot; . $e-&gt;getMessage()); 
    }


    $con-&gt;setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $con-&gt;setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

    if(function_exists(&#39;get_magic_quotes_gpc&#39;) &amp;&amp; get_magic_quotes_gpc()) { 
        function undo_magic_quotes_gpc(&amp;$array) { 
            foreach($array as &amp;$value) { 
                if(is_array($value)) { 
                    undo_magic_quotes_gpc($value); 
                } 
                else { 
                    $value = stripslashes($value); 
                } 
            } 
        } 

        undo_magic_quotes_gpc($_POST); 
        undo_magic_quotes_gpc($_GET); 
        undo_magic_quotes_gpc($_COOKIE); 
    } 

    header(&#39;Content-Type: text/html; charset=utf-8&#39;); 
    #session_start();
?&gt;</code></pre><p>이 파일을 위 Filezilla의 html 폴더에 넣어준다. 요렇게!<img src="https://images.velog.io/images/jinny_0422/post/b68b6fa8-e942-40ba-b67e-c447afc583bc/image.png" alt=""></p>
<h1 id="👩💻-실행-결과">👩‍💻 실행 결과</h1>
<p><a href="http://FTP%EC%95%84%EC%9D%B4%EB%94%94.dothome.co.kr/insertdata.php">http://FTP아이디.dothome.co.kr/insertdata.php</a>
로 들어가보면 이름과 학번을 입력할 수 있다.
<img src="https://images.velog.io/images/jinny_0422/post/501ef7cc-d015-4638-b10c-64d1c6286b53/ffffsssss.gif" alt="">
데이터가 잘 들어와 있는 걸 확인 할 수 있다.
<img src="https://images.velog.io/images/jinny_0422/post/2ceeaa33-f34b-4d61-b251-347511da06ec/image.png" alt=""></p>
<p>이제 안드로이드에서 이 데이터베이스를 사용하는 일이 남았다!
다음 포스팅에서 계속...!</p>
<hr>
<h5 id="참고--닷홈--파일질라-참고--php-출처">참고 : <a href="https://velog.io/@xyunkyung/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EB%8B%B7%ED%99%88">닷홈 + 파일질라 참고</a> &amp; <a href="https://omty.tistory.com/32">php 출처</a></h5>
]]></description>
        </item>
    </channel>
</rss>