안드로이드/Firebase

기존에 알림기능을 만든 프로젝트를 firebase콘솔에 등록해야한다.


1. 나는 기존에 쓰던 콘솔 프로젝트를 안드로이드스튜디오 >Tools>Firebase > Connect >를 통해 기존 사용하던 프로젝트에 앱(안드로이드 프로젝트명)을 등록했다.


2. connect가 완료되면 우측 Assistant에서 [ Add FCM to your app]을 통해 gradle을 추가해준다.(자동 추가된다!)


3. 이제 [Access the device registraion token]에서  2번째 토큰얻어 서버에 인증해주는 서비스로 추가해주어야 하지만, 

   오늘은 생략할 것이다. 

 <service>있는 부분은 [나를 서버에 인증하는 코드]로, 서버에 [내이름]을 알려주는 것을 서비스를 통해서 하는 것이다.


4. [Handle Message]부분을 보자 공식문서의 [메세지 수신]부분과 동일한 부분이다.

(1) <service>있는  부분은 [푸쉬가 오는 것을 받아주는 귀]로서 서비스에서 듣고 있는 것이다. <서비스를 추가할 때는, application 안 / activity 밖! >


<service

    android:name=".MyFirebaseMessagingService">

    <intent-filter>

        <action android:name="com.google.firebase.MESSAGING_EVENT"/>

    </intent-filter>

</service>


(2) 빨간색이 뜨는  android:name=".MyFirebaseMessagingService"는 MyFirebaseMessagingService클래스가 없다는 뜻이다. 자바패키지폴더에서 우클릭으로 서비스를 만들어준다. 패널을 보면, 서비스는 FirebaseMessagingService를 상속하라고 나온다.  

기본 서비스 생성으로 extends Service 되어있는 것을 extends  FirebaseMessagingService 로 바꿔주자.


public class MyFirebaseMessagingService extends FirebaseMessagingService {


(3) 패널을 보면, 서비스 안에서, onMessageReceived()만들라고 하면서 코드가 있다. 이때, 코드를 복사해오지말고(3번을 생략했으니 코드꼬임)

   서비스 내부에서 [alt + insert]로 onMessageReceived()만 오버라이딩 하자

     

(4) 파라미터의 remoteMessage.로 넘어오는 메세지를 받을 수 있다. remoteMessage.getNotification().getBody() 를 통해 콘솔에서 보낸  

  문자열로 넘어온다. println으로 찍어주자.


public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override

    public void onMessageReceived(RemoteMessage remoteMessage) {

        super.onMessageReceived(remoteMessage);


        System.out.println(remoteMessage.getNotification().getBody()); 


    }


(5) 테스트를 위해 앱을 실행시키고, firebase콘솔에서 메세지를 보내는 테스트를 해보자.



5. firebase 콘솔로 와서, Notification 메뉴를 눌러 메세지를 보내보자.


 - 메세지내용만 적고 라벨은 안적어도된다.(확인하는 이름이고, 사용자에게는 안간다. 스터디/공지/ 이런 분류로 넣어줘도된다 맘대로)

 - 대상에 보면, 앱을 선택할 수 있다. 등록된 앱을 선택하자.

 > 메세지를 보내면, 따로 호출안해도 백그라운드에서 자동 실행되는 서비스로서, 푸쉬가 오는 것을 받아, logcat에 찍어주는 것을 확인할 수 있다

    logcat에서는 sys만 적어서 검색을 통해 한눈에 볼 수 있게 하자.


6. 이제 헤드업알림(notification8())을 통해 푸쉬를 받는 코드를 작성해보자.

(1) notification8()코드를 복사해서, 만들어준 서비스 MyFirebaseMessagingService 클래스 하단에다가 집어넣자.


(2) 로그캣에 찍어준 remoteMessage.getNotification().getBody()는 String으로 받아주자.

        //System.out.println(remoteMessage.getNotification().getBody());

        String bodyStrFromServer = remoteMessage.getNotification().getBody();


(3) 푸쉬텍스트를 받은 bodyFromServer 를 builder의 setContentText에다가 집어 넣자. title을 코드로서 띄울 것이니 넣고싶은것을 넣자.

                        .setContentTitle("Social MPS FLEX")                     //fcm에서는 내용만 받는다. 타이틀은 임의로 Social MPSFLEX라 짓자

                        .setContentText(bodyStrFromServer )                     //Fcm에서 받은 내용을 집어넣었다.




7. 에뮬을 실행시키고 콘솔에서 다시 메세지를 띄워 확인해보자.

(헤드업알림은 단순알림+앱켜져있으면 헤드업에 열리는 기능이다. 앱이 꺼져있으면 단순알림처럼 온다.)



앱이 꺼져있는 상태에서 보냈을 경우



앱이 켜져있는 상태에서 보냈을 경우






1. 기존예제에 이어서 메인액티비티 xml에 오늘 실습할 버튼4개를 추가로 더하고,  자바에서 처리도 기존처럼하자.



2. [독립창]을 실습해보자. 

단순알림은 기존에 있던 앱의 화면을 덮어써서, 뒤로가기를 누르면 앱도 꺼지는 것이고

멀티뷰알림는 앱을 덮어쓰진 않지만, 기존의 액티비티에 종속시킨 액티비티로서, 뒤로가기가 구현된다.

독립창은 기존의 창이 아래로 깔리고, 전혀 새로운 창이 뜨는 것이다.

문서의 [특수 액티비티 PendingIntent 설정]을 참고하면 된다.


(1) 먼저 매니페스트 설정하는 코드가 나온다. 기존에 ResulActivity는, 단순알림->최종 [멀티뷰 알림]에서 사용했으니, 

  [ ThirdActivity ] 로 액티비티를 만든다.

(2) 매니페스트 설정하는 코드를 ThirdActivity에 적용한다. 

 - 첫번째 launchMode속성의 [singleTask]로 지정한 것이 바로 특수액티비티로 만들겠다는 표현이다.

   (종속된 것이 아닌 기존켜진 앱을 아래로 깔고, 새로운 특수액티비티 띄움)

 - taskAffinity속성에 아무것도 안 넣는 것("")은 개발자가 코드에서 설정하는 FLAG_ACTIVITY_NEW_TASK 플래그와 더불어 이 Activity가 애플리케이션의 기본 작업으로 들어가지 않게 보장합니다.

 - excludeFromRecents속성에 true를 주는 것은 사용자가 우연히 이곳으로 다시 이동하지 못하게 합니다.(제한을 거는 것이다)


        <activity android:name=".ThirdActivity"


            android:launchMode="singleTask"

            android:taskAffinity=""

            android:excludeFromRecents="true"></activity>


(3) 문서 바로아래의 자바코드 조각을 notification5()에 집어넣고, ResultActivity로 되있는 것을 ThirdActivity로 바꿔주고, 코드는 4로 준다.

(4) 또 이 예제에서 아이콘을 안줘놨다. 아이콘 지정하는 것을 builder에 지정해주자

     + 거기다가 .setAutoCancel(true);도 같이 넣어주자. 알림을 선택하면 없어진다.

       (새로운 화면을 띄우는 모든 알림에 넣어줘야될듯하다. 단순알림, 멀티뷰알림, 독립창알림 )


        NotificationCompat.Builder builder = new NotificationCompat.Builder(this)

                .setContentTitle("멀티뷰 알림") //제목


                .setContentText("4번째 멀티뷰 알림입니다.") //내용

                .setSmallIcon(R.mipmap.ic_launcher) //아이콘

                .setAutoCancel(true);                               //알림 선택시 새로운 새로운 액티비티가 띄워질때, 알림선택이 없어진다.



(4) 앱을 실행해서 확인한다. 띄워진 ThirdAcitvity는, 뒤로가기 구현도 안되있고, 종료시켜도 앱이 살아있다.





3. [로딩-가로]로 퍼센트를 채우는 알림을 실습해보자.(앱 설치시 나오는 것)

문서 [고정 기간 진행 상태 표시기 표시]를 참고하여 해당 코드를 다 복사해서 -> notification6() 에 넣어준다.

(1) 각종 클래스명, 및 쓰레드안에 들어가있는 변수에 final붙혀주기, 코드는 5로 준다.

(2)for (incr = 0; incr <= 100; incr+=5) { 를 보자. 

 - 0부터 100까진데, 5씩 늘어나니까 총 20 바퀴 돈다는 의미다. 5번만 돌도록 incr+=20으로 수정하자.

                       for (incr = 0; incr <= 100; incr+=20) {

 - 이제 내부에 sleep(5000)이 있다. 이것을 1000 = 1초로 바꾸자. 그럼 1번당 1초 니까 총 5초가 된다.

 



4. 로딩-rotatation은 문서 [지속적 액티비티 표시기 표시]를 참고한다. 점점 채워지는게 아니라 그냥 회전을 계속한다 

* 로딩-가로에서 실습했떤 코드를 그대로 가져온 뒤, 1줄만 수정해주어 무한히 돌아가도록 한다.

mBuilder.setProgress(100, incr, false);.

mBuilder.setProgress(0, 0, true);   //수정할 코드 mBuilder.setProgress(100, incr, false); -> 0,0,true 로 주어 무한히 돌아가도록 한다.



5. 마지막 [헤드업 알림]은 단순알림 코드를 복사해와서 몇가지만 수정해주면된다.

(1) notification() (단순알림)코드를 가져온다.

(2) builder에다가 새로운 설정을 더해준다.

                        .setPriority(Notification.PRIORITY_HIGH)    // 헤드업알림 추가코드1, 속성을 위로

                        .setDefaults(Notification.DEFAULT_VIBRATE); //  헤드업알림 추가코드2, 진동을 줌



(3) 만약 앱이 꺼진 상태라면, 단순알림처럼 알림이오고, 클릭시 앱이 켜진다.

1. 버튼 4개를 xml에 추가하고 id를 다 준다.

* 이때, constraint레이아웃에 추가할 때, 점들을 다 안이으면 앱실행시 겹쳐보인다. 레이아웃 상단에 [마법봉] - infer Constraints를 클릭하면 알아서 다 연결해준다.

* 그래도 실행해서 약간 겹쳐보이는 것은, 왼쪽에 [점연결 없애는 버튼] - All clear Constraint를 한번 눌렀다가 다시 [마법봉]을 클릭해서 연결해주자.


2. 버튼을 많이 만들 것이기 때문에 메인액티비티에 implements를 통해서 onClickListener (인터페이스)를 상속해서

 -> 빈 onClick()을 오버라이딩 한다. 

버튼에서는 리스너를 다는데 파라미터로 메인액티비티를 의미하는 this를 넣어(new OnClickListenr가 아님) ->

상속하는 onClickListener ()에 의해 구현화한 onClick()매쏘드로 타고 들어가, 안에서는 switch문에 버튼들의 id를 받아서, id에 따라 다르게 작동하도록 넣어준다.

   - alt+enter를 통해 onClick()매쏘드가 오버라이딩 된다. 여기다가, setOnclick리스너에 (new On~)으로 생성안하고, this를 통해 context만 넣어주자.

     여기서  this는 class를 가르킨다. class 메인액티비티는 상속을 통해 View.Onclicklistener의 필드와 매쏘드를 가지고있으니

     button이라는 것은 곧, 오버라이딩 된 onClick()까지 타고 들어가는 것이다. 이런 방법을 통해 버튼 많은 액티비티는 지저분해보이는 코드를 정리할 수 있다.

3. onCreate()버튼을 변수생성후 id도 바로 참조해준다.


4. 버튼들마다 set온클릭리스너를 달아주고, 파라미터로 new로 생성할 것이 아니라, this를 주어, 

메인액티비티->온클릭리스너->오버라이딩 된 onClick()매쏘드로 가도록 하자.


5. onClick()에서는 4개의 버튼에 달린 리스너가 호출될 때, 작동하는데, switch문의 필터링 부분에,  

파라미터의 view.를 이용해 각 버튼의 id를 얻어내는 getId()로 각각 다르게 행동하도록 하자.


6. 이제 각 id에 따라 case R.id.* ~break;를 넣어주는데, 버튼마다 작동할 알림코드를 넣을 notification()매쏘드를 또 따로 만들어주자. case문 안에 넣으면 또 어지러울 수 있다.


    @Override

    public void onClick(View view) {


        switch(view.getId()){


            case R.id.button:

                notification();

                break;

            case R.id.button2:

                notification2();

                break;

            case R.id.button3:

                notification3();

                break;

            case R.id.button4:

                notification4();

                break;

        }

    }


    private void notification() {


    }

    private void notification2() {


    }

    private void notification3() {


    }

    private void notification4() {


    }


7. 이제 알림에 대한 코드를  개발자 문서를 참조하자.

developer.android.com >notification 검색 > 클릭 >[ 단순알람 만들기]

- 나는 검색눌러도 무반응이길래  개발> api가이드 > 사용자 인터페이스 > [알림]까지 찾아들어왔다

(https://developer.android.com/guide/topics/ui/notifiers/notifications.html)

 - 더 쉬운 방법은 구글에서 'android notification'을 찾아서 문서로 들어가는 것이다.


8. [단순알림]은 제목+내용과 함께 클릭시 만든 액티비티로 넘어가는 알림이다. 

<4번째로 할  멀티뷰 알람과 차이점으로, 단순알림은 클릭하여 나온 액티비티에서, 뒤로가기 하면 앱전체가 꺼진다>

[단순 알림 만들기]문서의 코드를  notification()에 그냥 복사붙혀넣기 한다.

 각종 빨간줄을 import할 때는, v4밑에 있는 것을 한다.

(1) 아이콘이 없는 것은 임시로 R.mipmap.에 있는 런처아이콘으로 일단 붙혀넣는다.

(2) 그다음에 ResultActivity가 없다고 뜨는 것에 대해 자바패키지폴더안에 ResultActivity액티비티를 만든다.

*우클릭해도 되지만 [alt +insert]로 new창을 띄울 수 있다.

(3)xml파일로 가서, 컴포넌트트리의 ConstraintLayout을 클릭한 상태에서 속성창에 펼쳐보기 한 다음 배경색을 주자.

(4)추가적으로 빌더에 .setAutoCancel(true); 까지 셋팅해주어, 알림을 클릭하면 알림이 없어지도록 해준다.


=> 단순알림은 알림을 클릭하면 ResultActivity가 뜬다. 기존의 앱이 켜져있더라도, 앱이 덧씌워지는 구조다. 뒤로가기 누르면 앱이 꺼진다.



9. [상세알림]은 제목+ events배열에 담긴 문자열을 배열수만큼 쌓는다. <클릭해도 액티비티로 안넘어간다.>

문서의 [알림에 확장 레이아웃 적용]을 참고하여 notitfication2()에 가져온다.

(1) 아이콘을 바꿔주고, 중간 오타인 ;를 채워준다. 맨마직막의 inBoxStyle도 소문자로 고쳐준다.

(2) 여기에는 실행하는 코드가 빠져있다. 단순알림에서 있는 NotificationManger로 mBuilder를 실행시켜주는 코드를 가져와서 맨마지막에 붙혀준다.

    ***매니져의 첫번째파라미터인 코드를 단순알림과 똑같이 0으로 넣으면, 단순알림과 겹쳐진다. 코드를 일단 1로 주자.(notifyID 라고 한다)


        NotificationManager mNotificationManager =

                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        mNotificationManager.notify(0, mBuilder.build());  


(3) 단순알림과 다르게, <액티비티 띄우는 인텐트 -> 페딩인텐트에 담아서 빌더에 담는부분>이 빠져있다. 앱을 실행하여 알람을 클릭해도 새로운 액티비티로  넘어가지 않는다. 

단순알림은 액티비티(resultactivity)를 만들고 페딩인텐트를 빌더로 넘겨서, 알림클릭시 액티비티를 만들었지만, 여기서는 events배열에다가 정보를 넣는다.


        String[] events = new String[6];        


        events[0] = "1번째 이벤트";

        events[1] = "2번째 이벤트";

        events[2] = "3번째 이벤트";

        events[3] = "4번째 이벤트";

        events[4] = "5번째 이벤트";

        events[5] = "6번째 이벤트";


(4)추가적으로 빌더에 .setAutoCancel(true); 까지 셋팅해주어, 알림을 클릭하면 알림이 없어지도록 해준다.


10. 알람쌓기는 숫자가 뜨면서 쌓아져야 하는데 기종마다 차이가 있으므로 생략한다.

[알림 업데이트] 문서를 참고해서 notification3()에 넣어주자.

(1)mNotificationManager 변수만 있고 클래스명 빠진 곳에 적어주자.

(2)***mNotifyBuilder 는 클래스명이 NotifyBuilder이 아니라 NotificationCompat.Builder 이다. 주의하자.

(3) numMessages = 0; int를 앞에 적어주자.

(4)         mNotifyBuilder.setContentText(currentText)에서는 currentText를 삭제하고 문자열로 "내용"을 넣어주자.

(5) notifyID 따로 선언되어 파라미터로 넣어주는데, 나는 그냥 지우고, 코드2로 줬다.


11. 멀티뷰 알림은 단순 알림과 다르게, 알림클릭시 나오는액티비티에서 뒤로가기를 눌러도 꺼도, 

기존 앱이 그대로 남아있게 된다. [액티비티를 시작할 때 탐색 보존] 문서를 참고한다.

메니페스트에서 메인액티비티의 아래계층으로 줘서 뒤로가기눌러도 안꺼지고 유지되는 것 같다.


(1) 먼저 [정규 액티비티 PendingIntent 설정] 문서를 보고, 매니페스트에서 메인액티비티<-> 띄울 액티비티 간의 계층을 줘야한다. 

(단순알림에 쓰던 ResultActivity를 또 쓸 것이다. 나중에 단순알림을 재사용할 때 참고하자.)


(단순알림시 만든) <activity android:name=".ResultActivity"></activity> 라고 있던 것을


<activity

    android:name=".ResultActivity"

    android:parentActivityName=".MainActivity">

    <meta-data

        android:name="android.support.PARENT_ACTIVITY"

        android:value=".MainActivity"/>

</activity>

를 복사붙혀넣기하여 고친다. 메인액티비티를 부모액티비티로 설정하는 것이다.


(2) 동일문서의 아랫부분있는 인텐트관련 코드를 notification4()에다 짚어넣어준다. id코드는 3으로 줬다.

(3) *** 예제에 빠져있는, nNotifyBuilder에 제목/내용/아이콘/ 클릭제거 세팅을 다 가져온다.


                .setContentTitle("멀티뷰 알림") //제목

                .setContentText("4번째 멀티뷰 알림입니다.") //내용

                .setSmallIcon(R.mipmap.ic_launcher); //아이콘

.setAutoCancel(true); //클릭시 알림제거


(4)***** 멀티뷰를 켜기전에 한번 삭제해주자. 앱을 깐 이후 메니페스트관련 설정을 해줬다면, 앱을 한번 삭제시키고 설치해서 확인하자.!!

앱을 실행시켜보면, 알림창을 통해 띄운 ResultActivity를 종료하더라도 메인액티비티가 남아있는 것을 확인할 수 있다.



1. 저번시간에 이어서 AdMob>android문서의 [전면광고(interstitial)] 구현을 위해

  [Create an interstitial ad object]문서를 보자

cf) interstitial광고는 꼭! 버튼을 이용해서만 실행할 수 있다. banner와 다르게 load ad 이어서 show ad가 따로있다.

    버튼을 이용해서 띄우므로, xml이 따로 필요없다.


 (1)저번시간에 banner에 대해 onCreate()에 선언한 부분 중 모든 광고 공통인 sdk초기화 코드인 

    MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713");

   코드를 제외하고, 전부 잘라내서 void banner()매쏘드를 만들어 넣어준다.


    void banner(){


        mAdView = findViewById(R.id.adView);

        AdRequest adRequest = new AdRequest.Builder().build();

        mAdView.loadAd(adRequest);

        mAdView.setAdListener(new AdListener() {

            

...


}



(2)[ Create an interstitial ad object ] 문서의 자바코드 중 색칠 된 부분만 복사해온다. 전역변수 / onCreate()에서 id참조.


    private InterstitialAd mInterstitialAd;


        mInterstitialAd = new InterstitialAd(this);

        mInterstitialAd.setAdUnitId("ca-app-pub-3940256099942544/1033173712");



(3) 광고를 로드하는 부분인 [Load an ad]문서에서 색칠된 부분을 (2)에 이어서 그대로 붙혀넣어준다.


        mInterstitialAd.loadAd(new AdRequest.Builder().build());


(4)[show an ad]코드 중  mInterstitialAd.show(); 만 가져와도 된다. 버튼을 추가해 리스너에 코드를 추가한다.(반드시 버튼이 있어야 작동하는 광고다)


        button  = (Button) findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {


                mInterstitialAd.show();


            }

        });


(5) 앱을 실행해서 확인해본다. 버튼을 클릭하면 아래 광고가 전면으로 발생한다.




2. 교재는 Native Express를 설명하나, 폐쇄되고, Native Advanced로 나온다고 한다. 일단 Advanced로 진행하려면, [문서 전체의 탭] > [Sample]로 가서 코드를 보면서 해야한다. 생략한다.

<https://github.com/googleads/googleads-mobile-android-examples/tree/master/java/admob/NativeAdvancedExample>



3. [ Rewarded Video ]를 보자. 마찬가지로 버튼 클릭으로 실행되어야한다.


(1) 전역변수로 변수 선언, sdk초기화 코드 밑에 인스턴스 참조 코드를 가져온다. 이때, 아래 this에 빨간줄이 뜨는 것은 리워드비디오리스너 인터페이스를 implements해야한다 alt+enter로 한번에 implements하자. 여러개 매쏘드도 같이 들어오게 된다.


    private RewardedVideoAd mRewardedVideoAd;


       // Use an activity context to get the rewarded video instance.

        mRewardedVideoAd = MobileAds.getRewardedVideoAdInstance(this);

        mRewardedVideoAd.setRewardedVideoAdListener(this);



(2)  인터페이스 상속 결과 나타나는 매쏘드들은 클릭으로 앱이 잠시 떠날 때, 비디오닫혔을때, 로딩실패시, 로딩준비완료시, 클릭시, 비디오시작시의 매쏘드들이 오버라이딩 된다.***이때, onRewardedVideoAdLoaded()만 필요하다. <버튼을 누르기전, 광고를 띄우기직전의 화면에서> 비디오광고가 시작할 준비가 되었을 때 토스트메세지로 알려준다.

public class MainActivity extends AppCompatActivity implements RewardedVideoAdListener {

...



    @Override

    public void onRewardedVideoAdLoaded() {


        Toast.makeText(this, "onRewardedVideoAdLoaded", Toast.LENGTH_SHORT).show();



    }



(2) [Request rewarded video ad] 에서 load하는 부분만 복사하자. 매쏘드는 안쓸 것임. onCreate()에 이어서 붙혀주자.

-    mRewardedVideoAd.loadAd("ca-app-pub-3940256099942544/5224354917", new AdRequest.Builder().build()); 이부분만 쓴다.


private void loadRewardedVideoAd() {


    mRewardedVideoAd.loadAd("ca-app-pub-3940256099942544/5224354917",

            new AdRequest.Builder().build());

}


(3) 비디오 광고를 열어줄 버튼2를 만들어주고, [Display an ad]부분에서 show()해주는 코드만 가져와 리스너에 넣는다.

- mRewardedVideoAd.show();


        button2  = (Button) findViewById(R.id.button2);

        button2.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {


                mRewardedVideoAd.show();


            }

        });






***실제 앱이 잘나가면, 구글광고대신 업체를 찾아야한다. 대학생이라 시간이 없으면 구글애드만 달아놓자.



 firebase콘솔 왼쪽메뉴 [AdMob] 으로 간다.  ([가입]을 누르면, 광고아이디를 받을 수 있은나, 오늘은 테스트 코드로만 진행해보자.)


1. 프로젝트를 만든다(AdMob) -> 패키지명만 복사해놓는다.

2. 콘솔 상단의 설정아이콘을 눌러 [앱 추가] 에 패키지명을 넣고,  google-service.json을  <보기방식>[project]로 바꾸어 app폴더에 집어넣는다.

3. 첫번째gradle의 디펜던시 안에 classpath와  두번째gradle의 디펜던시 밖에 플러그인코드를 붙혀넣는다.

* firbase-core는 넣어봤자 동기화가 안된다. 무시하고 지나가자

4. 문서> [AdMob] > android로 와서, sdk(광고 라이브러리)를 complie하자.

 - compile 'com.google.firebase:firebase-ads:11.8.0'

5. [SDK 초기화] 문서에서 sdk를 앱이 시작할 때(onCretae()), 초기화해준다. 아래코드를 복사해서 onCreate()에서 호출해준다.

 이때,  2번째 파라미터로 "문자열"에 <애드몹 아이디 : YOUR_ADMOB_APP_ID>가 들어간다. 회원가입을 하면 주어지겠지만, 주석에있는 샘플id를 넣자.


// Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713

        MobileAds.initialize(this, "YOUR_ADMOB_APP_ID");


   -  MobileAds.initialize(this, "ca-app-pub-3940256099942544~3347511713");


6. 문서를 내리면 여러종류의 광고가 있는데, 배너만 실습해보자.


(1) [배너구현]에 들어가서 xml코드 중 색칠된 코드만 가져온다.

*속성중 adUnitId는 <배너에 대한 id>이다. 

<com.google.android.gms.ads.AdView

            xmlns:ads="http://schemas.android.com/apk/res-auto"

            android:id="@+id/adView"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_centerHorizontal="true"

            android:layout_alignParentBottom="true"

            ads:adSize="BANNER"

            ads:adUnitId="ca-app-pub-3940256099942544/6300978111">

        </com.google.android.gms.ads.AdView>


(2) 액티비티의 xml 레이아웃 안에다가 붙혀넣자.

(3) 감싸주는 레이아웃을 linear레이아웃으로주고, gravity를 center로 줘서 광고를 중앙에 위치하게 하자.

   문서를 내리면, xml이 아니라 java코드로 주는 방법도 있는데 생략한다.

(4) [Load an ad] 문서를 보고  xml에 대한 자바 변수 선언 / id참조 /  로드 를 해주는 코드를 가져온다.


 private AdView mAdView;


        mAdView = findViewById(R.id.adView);

        AdRequest adRequest = new AdRequest.Builder().build();

        mAdView.loadAd(adRequest);


************************************************************

id가 있는데도 못찾는 에러가 발생했다. clean build했다가, 

Error:Execution failed for task ':app:processDebugGoogleServices'.

> No matching client found for package name 'com.example.cho.admob'

에러가 났다. 

검색해보니, google-services.json 파일이, 이전 프로젝트에 사용을 위해 다운받았던 json파일 이었다.

보기방식을 [Project]로 다시 바꾸어, app폴더의 json파일에 가보면, 패키지명이 지금과 다를 것이다.

다음부터는 새로 다운 받은 것을 잘라내기로 가져와서 넣어주자.(삭제 안되면 안스 재부팅)

************************************************************

cf) clean 과 rebuild 프로젝트 차이점 : https://stackoverflow.com/questions/24083706/difference-between-clean-project-and-rebuild-project-in-android-studio


(5) 여기까지만 해도 바로 실행이 된다. 하지만 [문서] 하단에 리스너를 다는 부분도 있다.

 - onAdLoaded()는 광고가 로딩될때의 리스너다.

- onAdFailedToLoad() 광고 로딩이 실패할 때,

 - onAdOpened() 광고가 클릭될 때 , 

 - onAdLeftApplication() 광고가 클릭되어 앱이 잠깐 뒤로 가서, 멈출 때,

 - onAdClosed() 광고를 닫을 때,


mAdView.setAdListener(new AdListener() {

    @Override

    public void onAdLoaded() {

        // Code to be executed when an ad finishes loading.

    }


    @Override

    public void onAdFailedToLoad(int errorCode) {

        // Code to be executed when an ad request fails.

    }


    @Override

    public void onAdOpened() {

        // Code to be executed when an ad opens an overlay that

        // covers the screen.

    }


    @Override

    public void onAdLeftApplication() {

        // Code to be executed when the user has left the app.

    }


    @Override

    public void onAdClosed() {

        // Code to be executed when when the user is about to return

        // to the app after tapping on an ad.

    }

});



[프로젝트 : DynamicLink ]


저번시간에 만든 <동적링크(DynamicLinks) 속 딥링크(json등 정보를 담은 url) >을 받은 다음 정보를 파싱할 것이다.

만약, 맛집 컨텐츠(json정보)를 담은 DynamicLinks --> 사용자 --> 앱에서 json정보를 파싱해서 보여줌. 

식으로 만들면 유용할 것이다.


1. 문서로 가서, [동적 링크] > [동적링크 수신] > [Android]로 이동해오자.

(1)[딥 링크에 대한 인텐트 필터 추가] 문서에서 아래 인텐트필터를 복사하자

(사실 공부다하고, 이부분을 주석처리하고 동적링크로 딥링크를 받아봤는데,, 잘 받아진다ㅠ)


<intent-filter>

    <action android:name="android.intent.action.VIEW"/>

    <category android:name="android.intent.category.DEFAULT"/>

    <category android:name="android.intent.category.BROWSABLE"/>

    <data android:host="yoursite.example.com" android:scheme="http"/>

    <data android:host="yoursite.example.com" android:scheme="https"/>

</intent-filter>


(2) 이것을 manifest.xml에서 <이벤트를 발생시키는 액티비티(여기서는 메인액티비티)> 안쪽에다가 인텐트필터를 추가적으로 붙혀넣어준다.

(3) 코드 중 <data android:host="yoursite.example.com" android:scheme="https"/> 의 호스트 값은 콘솔 상단에 나와있는 

[동적링크 도메인]에서 https://를 제외한 부분이다.(qk5k8.app.goo.gl)

이 값이랑 호스트랑 매칭이 되면, 해당 앱이 켜진다. 즉, 호스트값과 일치한다면 앱을 켜주는 인텐트필터다.

* 이때, http와 https가 있는데, https만 쓸 것이기 때문에 http라 적힌 줄은 지워준다.

* 이때, 콘솔에는 https://까지 같이 복사되는데, 삭제하고 넣어준다.


            <intent-filter>

                <action android:name="android.intent.action.VIEW"/>

                <category android:name="android.intent.category.DEFAULT"/>

                <category android:name="android.intent.category.BROWSABLE"/>

                <data android:host="qk5k8.app.goo.gl" android:scheme="https"/>

            </intent-filter>


2. 저번시간에 onCreate()에서 넣은 동적링크 생성코드를 따로 매쏘드createDeepLink()를 만들어서 빼주고, 동적링크 생성이 필요할 때, 호출해준다.


void createDeepLink(){

DynamicLink dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse("http://date.jsontest.com/")) //딥링크(정보를 담음) - json양식을 가진 맛집페이지
.setDynamicLinkDomain("qk5k8.app.goo.gl") //동적링크 도메인 - 콘솔에서 주어짐.
// Open links with this app on Android
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) //패키지명
.buildDynamicLink();

Uri dynamicLinkUri = dynamicLink.getUri();

System.out.println(dynamicLinkUri.toString());

Task<ShortDynamicLink> shortLinkTask = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse("http://date.jsontest.com/"))
.setDynamicLinkDomain("qk5k8.app.goo.gl")
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) //패키지명

.buildShortDynamicLink()
.addOnCompleteListener(this, new OnCompleteListener<ShortDynamicLink>() {
@Override
public void onComplete(@NonNull Task<ShortDynamicLink> task) {
if (task.isSuccessful()) {
// Short link created
Uri shortLink = task.getResult().getShortLink();
Uri flowchartLink = task.getResult().getPreviewLink();

System.out.println(shortLink.toString());

} else {
// Error
// ...
}
}
});

}


3. [딥 링크 처리] 문서에 보면, 동적링크에 의해 들어오는 딥링크를 가져오는 코드가 있다. 

onCreate()에서 호출 한뒤, 리스너에서 대기하다가 받아주게 해주자.


딥 링크를 수신하려면 getDynamicLink() 메소드를 호출합니다.


FirebaseDynamicLinks.getInstance()

        .getDynamicLink(getIntent())

        .addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {

            @Override

            public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {

                // Get deep link from result (may be null if no link is found)

                Uri deepLink = null;

                if (pendingDynamicLinkData != null) {

                    deepLink = pendingDynamicLinkData.getLink();

                }



            }

        })

        .addOnFailureListener(this, new OnFailureListener() {

            @Override

            public void onFailure(@NonNull Exception e) {

                Log.w(TAG, "getDynamicLink:onFailure", e);

            }

        });


(1) 딥링크를 받은 성공리스너에다가 토스트메세지로, 받은 딥링크uri를 toString()을 붙혀서 띄워주자. 


FirebaseDynamicLinks.getInstance()
.getDynamicLink(getIntent())
.addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
@Override
public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
// Get deep link from result (may be null if no link is found)
Uri deepLink = null;
if (pendingDynamicLinkData != null) {
deepLink = pendingDynamicLinkData.getLink();

Toast.makeText(MainActivity.this, "호출된 딥링크 : "+deepLink.toString() , Toast.LENGTH_SHORT).show();

}


}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(MainActivity.this, "딥링크를 받지 못하였습니다.", Toast.LENGTH_SHORT).show();

}
});


(2) 에뮬레이터의 크롬에서 동적링크를 작동시켜보자. 

설치된 앱 실행 => 동적링크 처리 액티비티에서 대기중인 리스너의  토스트메세지에, 딥링크를 띄운다.





(3) 만든 짧은동적링크 (https://qk5k8.app.goo.gl/BkRX)를 앱의 browser에서 켜보면, 깔린 앱이 열리면서, 리스너가 작동한다.

앱을 지운상태라면, googleplay화면으로 가고, 앱이 있는 상태면 앱에서 동적링크를 받는 리스너가 그 속의 딥링크를 토스트 메세지를 띄운다.

** 이때, 크롬을 이미썼던것에 다시 동적링크 주소를 걸면,, 잘 안되는 경향이 있다. 크롬을 완전히 끈 뒤, 동적 주소를 입력하자.

뿌려주는 딥링크는 긴/짧은 동적링크가 아니라, setLink()에 넣은 딥링크만 나온다 



4. 이제 링크인 콘텐츠(json양식) 웹(uri)에서 데이터를 가져오는 작업을 해보자. byOkHttp3

(1) 메인액티비티 xml의 텍스트뷰에 id를 주고 자바코드로 텍스트뷰 변수와 id를 참조하자.

(2) 웹에서 데이터를 가져오는 라이브러리 가운데, < okhttp >를 쓸 것이다.

[app] 우클릭 > open module settings > + > library dependency 에서 okhttp를 검색하자

- 나는,,, 검색이 안된다. * 그래서 그냥 okhttp3를 검색했다.

 - compile 'com.squareup.okhttp3:okhttp:3.5.0'


(3)메인액티비티에 void getTime(String url)매쏘드를 만들어서, 내부에 OkHttpclient 객체를 만든다.

- 아래코드처럼 작성할 때, new Request.Builder에서 에러가 난다. new R에서 .Builder까지 한번에 달려있는놈을 선택하자. 

리퀘스트빌더객체.url을 처리해주고. build().로 닫아준다. 그리고  enqueue(new C로 콜백매쏘드를 완성하자.

void getTime(String url){

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(new Request.Builder().url(url).build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, final Response response) throws IOException {

new Handler(getMainLooper()).post(new Runnable() {
@Override
public void run() {

try {
JSONObject jsonObject = new JSONObject(response.body().string());

textView.setText(jsonObject.getString("time"));

} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}


}
});

}
});

}

(4) 성공리스너인 onResponse()에서는, 이제 UI인 테스트 를 바꿔줘야한다. 이때, okHttp3는 쓰레드를 통해서 통신하기 때문에 <핸들러>를 사용해서 

ui에 접근해야한다. * 핸들러의 파라미터에는 getMainLooper()를 넣어주자. 뒤에 .post()를 붙혀서 new Runnable을 붙혀서 run()매쏘드를 만들자.


(5) url에는 json정보가 담겨있다. 먼저 JSONObject 객체를 만들어준다. 생성시 파라미터에 바로 url속 json정보들을 담을 수 있다.

     넘어오는 response.을 이용해 .body().string()를 호출하여 url속 json정보를 받로 받아온다.

     이때 try/catch로 한번 잡아줘야한다.


(6)이제 jsonObject에 담긴 정보들을 setText해준다. 웹에서 확인한 json속에 왼쪽 키값이 String value를 담고 있는 time을 확인할 수 있다.


{
   "time": "08:02:47 AM",
   "milliseconds_since_epoch": 1518249767764,
   "date": "02-10-2018"
}

                            JSONObject jsonObject = new JSONObject(response.body().string());


                            textView.setText(jsonObject.getString("time"));



=> url속 json 키값 "time" -> okhttp3 의 response.body() .toString() ->

     jsonObject = new JSONObject(response.body().string()); -> jsonObject.getString("time")



====================================================

참고) getMainLooper() : http://codetravel.tistory.com/19


getMainLooper() 함수는 Main Thread(UI Thread)가 사용하는 Looper 즉 Main Looper를 반환합니다.

이 함수는 호출하는 스레드가 메인 스레드이거나 메인 스레드가 아니어도 언제든지 Main Looper를 반환합니다.

참고 1) Main thread의 Main Looper와 Handler는 ActivityThread에서 자동으로 생성하기 때문에 개발자가 명시적으로 생성하지 않습니다.

참고 2) Looper.myLooper() 함수는 호출한 스레드의 Looper를 반환합니다.


크게 3가지의 경우에 사용합니다.

1. Handler를 생성할 때 Main Looper를 생성자의 인자로 전달하고 싶을 경우

즉, 작업자 스레드(UI thread가 아닌 스레드)에서 UI thread에게 "Runnabel 작업" 또는 "메시지"을 보내고 싶을 때 사용할 수 있는 방법입니다.

Main Looper를 인자로 넣으면 Main Looper의 Queue를 mQueue로 설정합니다. 이 부분이 중요합니다.

결론적으로 sendMessage() 또는 post()를 하게 되면 mQueue에, 즉 Main thread의 큐에 들어갑니다.

2. 현재 스레드의 루퍼가 Main Looper인지 아닌지 검사하고 싶을 경우

3. 현재 스레드가 Main thread(UI thread)인지 아닌지 검사하고 싶을 경우


===================================================





5. 이제 동적링크를 통해 넘어오는 딥링크의 url 주소를 받아서 시간을 파싱하는 getTime()매쏘드를,   

딥링크를 담은 동적링크를 받아 처리해주는 리스너에 넣어준다.


                            Toast.makeText(MainActivity.this, "호출된 딥링크 : "+deepLink.toString() , Toast.LENGTH_SHORT).show();

                            getTime(deepLink.toString());



6. 이제 앱을 켜서, 앱의 외부 크롬에서 동적링크를 실행시켜본다. 앱이 실행되면서, 동적링크속 딥링크-json데이터를 파싱해서 textView에 뿌려준다.




7. 파이어베이스 콘솔에서 동적링크를 만들자.!!! (앱상에서 만들어 logcat찍는 주소는 콘솔에 등록이 안된다!)

- 마지막 [소셜 메타 태그 추가]는 카톡 등에서 주소를 주면, 미리보기 보여주는 것 처럼, 미리 보여주기 하는 거다.

- UTM매개변수는 마케팅 통계를 낼 때 쓰는 것이니 일단 넘어가자.

* 다이나믹 링크는 [클릭]을 통해 타고 드러가야한다. 그래서 크롬에서 잘 안됬음. 

어떠한 정보(json)를 가진있는 [1. 딥링크]와 firebase콘솔에서 주어지는 [2. 동적링크 도메인]를 담아서

안드로이드스튜디오의 <logcat> or <firebase콘솔>을 통해 만들어지는 [긴or짧은 동적링크]는  

firebase동적링크에 등록된 앱의[3. 패키지명]을 통해서, 


앱을 실행시켜 <호출한 액티비티에서 딥링크>를 열어준다. 

만약 넘어오는 딥링크가 json형식이면 okhttp등으로 파싱해서 이용한다.

만약 앱이 설치되지 않다면, [동적링크]+[패키지명]을 통해 <구글스토어 다운로드 페이지>를 열어준다.


*결과적으로 코드로 동적링크를 생성하는 것보다 firebase콘솔에서 생성하는 것이 더 편하다.



1. 새로운 프로젝트를 만들었다. 그러면 firebase콘솔에 새로운 안드로이드 프로젝트를 <앱>으로 등록해야한다. 

(0) 콘솔메인화면에서의 프로젝트는 안드로이드 프로젝트가 아니다.<안드로이드or iOS or 웹 등의 프로젝트 단위를 의미> 

기존에 사용하던 Chojaeseong (안드로이드)로 로그인하자.

(1) firebase콘솔의 좌측상단의 [설정]아이콘 선택-> [내 앱]의 [앱 추가] -> [android앱에 firebase추가]를 선택하여 빠르게 추가 할 수 있다.

(2) 패키지 이름은, MainActivity.java 의 맨 윗줄 패키지 부분을 복사해서 가져오면 된다. 패키지명만 입력하고 바로  계속을 누르자

(3) googles.services.json 파일을 받아서 복사한 상태에서

(4) 안드로이드 프로젝트 보기 방식을 [Project]로 바꾼 뒤, <app폴더>에 붙혀넣어준다.

(5) 이제 firebaseSDK를 아래 그림을 보고 보기방식을 [Android]로 돌리고서,  gralde을 통해서 추가하여, Sync now해주자. 

(6) 그러면 콘솔 [내 앱]에 새로만든 프로젝트가 추가되어있다.


2. 이제 문서로 이동하자. [android] > [동적 링크] > [동적링크 만들기] > [Android]까지 타고 들어오자. 

(교재는 옛날버전으로서, 한글판이 없어서 영어버전으로 보았다)

(1) [ Firebase 및 동적 링크 SDK 설정] 문서에 gradle로 complie해줄 부분을 해주자.

    compile 'com.google.firebase:firebase-invites:11.6.2'

*** 이때, [내 앱]에 추가할 때, complie했던  firebase-core의 버전과 같에 설정하자. 안하면 오류뜬다.


<여기까지 하고 난 뒤, firebase콘솔에서 쉽게 동적링크를 만들 수 있다 >


(2) [ Firebase 및 동적 링크 SDK 설정] 문서에 보면 빨간줄로 app_code.app.goo.gl 이라고 나와있다. 

이 app_code는  Firebase콘솔 > DynamicLinks를 클릭해서 들어가면 상단에 있다. 클릭해서 url를 복사할 수 있다.


(3) [ 매개변수로 동적 링크 만들기]문서의 아래코드를 보자. <결과적으로 긴 동적링크를 생성하는 코드>

 - .setLink(Uri.parse("https://example.com/")) 는 [1.딥링크]로써, json등 앱에서 사용할 정보를 담고 있는 주소다.

 -  .setDynamicLinkDomain("abc123.app.goo.gl") 는 [2.동적링크 도메인]으로써, 최종 다이나믹 링크 주소를 만들때, 앞부분에 들어간다.

 - .setAndroidParameters(new DynamicLink.AndroidParameters.Builder().build()) 은 [3.패키지명]을 담는 빌더로서, 앱을 실행시키거나 구글 스토어로 이동시킨다.

 - .setIosParameters(new DynamicLink.IosParameters.Builder("com.example.ios").build()) 는 ios스토어로 가도록 한다(하나의주소로 모두 다 가능하게).. 나는 생략


DynamicLink dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()

        .setLink(Uri.parse("https://example.com/"))

        .setDynamicLinkDomain("abc123.app.goo.gl")

        // Open links with this app on Android

        .setAndroidParameters(new DynamicLink.AndroidParameters.Builder().build())

        // Open links with com.example.ios on iOS

        .setIosParameters(new DynamicLink.IosParameters.Builder("com.example.ios").build())

        .buildDynamicLink();


Uri dynamicLinkUri = dynamicLink.getUri();


3. 이제 메인액티비티에서 링크를 만드는 작업을 해보자.

(1) onCreate로 가서 위에 코드를 복붙한다.

(2) 이제, 에뮬레이터에서 실행하는데, 에뮬레이터는 플레이스토어가 없다. 그러므로 앱이 설치 안되어있을 때, [패키지명]을 받아와 구글의 플레이스토어로 연결하여 설치하도록하는 setAndroidParameters() Builder파라미터에, [나의 패키지명 ]을를 넣자.

해당 주소는 Builder()의 파라미터에 "com.example.cho.dynamiclink" 적는 것이다.

-ios경우는 카톡 주소 자체를 알려주지 않기 때문에 그냥 삭제해서 생략하자.



.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) //패키지명


DynamicLink dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse("http://date.jsontest.com/")) //딥링크(정보를 담음) - json양식을 가진 맛집페이지
.setDynamicLinkDomain("qk5k8.app.goo.gl") //동적링크 도메인 - 콘솔에서 주어짐.
// Open links with this app on Android
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) //패키지명
.buildDynamicLink();



(3) 이제 예시로 적혀있는 setLink()를 수정하자. 정보를 담고 있는 [딥링크]를 적는 곳이다. 

나는 임시로 시간정보를 담고 있는 json테스트 사이트를 넣었다. 사이트를 크롬에서 열면 아래 정보가 나온다.

{
   "time": "07:14:13 AM",
   "milliseconds_since_epoch": 1518246853923,
   "date": "02-10-2018"
}


(4) setDynamicLinkDomain()가 바로 firebase콘솔의 DynamicLinks 상단에 나와있는 [동적링크 도메인]이다. 복사해서 가져오자.

*** 이때 https://qk5k8.app.goo.gl 중에 https://는 삭제하고 대입한다.


                .setDynamicLinkDomain("qk5k8.app.goo.gl")



(5)여기 까지 설정하면 맨아래의 dynamicLinkUri변수에 다이나믹링크 주소가 할당된다. uri를  .toString()을 붙혀서 로그로 찍어보자

* 이때 logcat의 Verbose 상태에서, system을검색하여 바로 찾아보자.


        System.out.println(dynamicLinkUri.toString());


(6) 로그에 찍힌 주소를 우클릭으로 uri복사해서, 실제 에뮬레이터의 크롬에서 붙혀넣어보자.



(7) 아직 동적링크를 받아주는 인텐트필터 처리 등 액티비티에서 처리하는 과정을 하지않았기 때문에

크롬에서 동적링크 입력 -> 로딩 -> 앱이 설치되어있으면 아래 그림처럼 열림(하지만 아직 받은정보를 처리하진 않는다. 다음시간에 함)




4. 짧은 주소를 만들기위해, 같은 문서의 아래 코드를 복사해서, onCreate()에 이어서 붙혀준다.

***자세히보면, .setLink 등 2개만 있고, 패키지명을 입력하는 매쏘드setAndroidParameters()가 빠져있다. 

파란색 코드를 따로 붙혀줘야한다.!!!


Task<ShortDynamicLink> shortLinkTask = FirebaseDynamicLinks.getInstance().createDynamicLink()

        .setLink(Uri.parse("https://example.com/"))

        .setDynamicLinkDomain("abc123.app.goo.gl")

.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) 

//패키지명 넣는 부분이 빠져있다.

        // Set parameters

        // ...

        .buildShortDynamicLink()

        .addOnCompleteListener(this, new OnCompleteListener<ShortDynamicLink>() {

            @Override

            public void onComplete(@NonNull Task<ShortDynamicLink> task) {

                if (task.isSuccessful()) {

                    // Short link created

                    Uri shortLink = task.getResult().getShortLink();

                    Uri flowchartLink = task.getResult().getPreviewLink();

                } else {

                    // Error

                    // ...

                }

            }

        });



(1) setLink()와 setDynamicLinkDomain()는 위에서 했던 그대로 넣어준다.

(2) 내부의 완료리스너에 있는 Uri shorLink에 짧은 주소가 담긴다. 

위에 작성했떤 println은 지우고, 이것을 println으로 찍어주자.


Task<ShortDynamicLink> shortLinkTask = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse("http://date.jsontest.com/"))
.setDynamicLinkDomain("qk5k8.app.goo.gl")
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.example.cho.dynamiclink").build()) //패키지명

.buildShortDynamicLink()
.addOnCompleteListener(this, new OnCompleteListener<ShortDynamicLink>() {
@Override
public void onComplete(@NonNull Task<ShortDynamicLink> task) {
if (task.isSuccessful()) {
// Short link created
Uri shortLink = task.getResult().getShortLink();
Uri flowchartLink = task.getResult().getPreviewLink();

System.out.println(shortLink.toString());

} else {
// Error
// ...
}
}
});


(3) 앱을 실행해서, logcat에 찍어준다.


더 짧아진것을 확인할 수 있다.



  1. 질문 2020.09.22 12:05

    유용한 글 잘봤습니다. 포스팅 하신지 좀 지났지만 여쭤보고싶은게 있는데 가능할까요??


RemoteConfig를 사용하면 원격으로 환경설정 값을 컨트롤 할 수 있다. 대표적으로 배경색 조절 or 준비된 프로세서를 켜거나 끌 수 있다.


1. 파이어베이스 콘솔 문서로 가서, android>시작하기> [원격 구성] > [샘플 앱 둘러보기]로 가서 코드를 가져오자.(기초 원격구성 사용은 pass)

(문서 링크 https://firebase.google.com/docs/remote-config/android?authuser=0)


(1)  필수조건의firebase-config를 gradle 추가 에서 아래 문장을 모듈에 추가해준다. (버전을 맞춰준다)

 - compile 'com.google.firebase:firebase-config:11.4.2'


(2) [작동 원리] 문서로가서 아래의 <원격구성 개체 인스턴스를 가져옴> +<세팅을 통해 새로고침이 자주 가능하게 함> 코드를 복사한다.


작동 원리

샘플은 우선 원격 구성 개체 인스턴스를 가져오고 캐시를 빈번하게 새로고칠 수 있도록 개발자 모드를 사용 설정합니다.

mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()
       
.setDeveloperModeEnabled(BuildConfig.DEBUG)
       
.build();
mFirebaseRemoteConfig
.setConfigSettings(configSettings);


2. homeActivity로 와서 액티비티의 함수로 remote_config()매쏘드를 만들어 복사한 코드를 붙혀넣자.


    private void remote_config(){


        mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();

        FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()

                .setDeveloperModeEnabled(BuildConfig.DEBUG)

                .build();

        mFirebaseRemoteConfig.setConfigSettings(configSettings);


    }


(1) 빨간줄 뜨는 변수를 액티비티의 전역변수로 선언해주자. 

private FirebaseRemoteConfig mFirebaseRemoteConfig;


(2) 다시 공식문서로 와서 [작동원리]의 2번째 <인앱에서 xml파일에 매개변수 기본값을 저장한 것을 불러오는 설정해주는 코드>를 가져와 remote_config() 매쏘드내부 아래쪽에 이어서 붙혀넣는다. 그러면 아직 업는 R.xml에서 빨간줄 에러가 뜬다.


공식문서)

그런 다음 XML 파일에서 인앱 기본값을 설정합니다.  setDefaults()를 사용하여 원격 구성 개체에 xml속 매개변수와 값을 값을 추가해준다.

mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);

    private void remote_config(){ //원격구성을 사용하게 해주기위해 만든 매쏘드. onCreate()에서 호출되어, 리스너에서 대기할 것이다.


        mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();

        FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()

                .setDeveloperModeEnabled(BuildConfig.DEBUG)

                .build();

        mFirebaseRemoteConfig.setConfigSettings(configSettings);


        mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);

    }



<해석>

위 매쏘드에서 RemoteConfig 변수에 인스턴스 참조하는 아랫부분부터 시작되는 아래코드는[ 디버깅 테스트할 때 사용이 된다. ] 

계속 서버에 요청을 하기 때문에 과부하가 초래될 수 있기 때문에 config에는 1분안에 3번이상 요청못하게 되어있다. 3번을 넘어가면 fail이 난다. 

그 fail이 나지 않도록 요청하는 것이 아래 코드이다. 아래 코드가 들어간다면, 3번이상 요청해도 fail이 나지 않는다. 


        /* 아래코드는 디버깅 테스트 할 때 사용한다.*/

        FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()

                .setDeveloperModeEnabled(BuildConfig.DEBUG)

                .build();

        mFirebaseRemoteConfig.setConfigSettings(configSettings);


이제 (2)에서 가져온 코드는 [서버의 매개변수와 매칭되는 값이 없을 때 사용되는, 기본값xml을 세팅해준다. ]다. 저 xml은 따로 만들어줘야한다.

        /*서버에 매칭되는 값이 없을 때 참조*/

        mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);



3. R.xml.remote_config_defaults에 대한 xml을 만들어주자.

(1) 먼저 공식문서에서 mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);  코드를 소개한 부분의  MainActiviti.java 링크(깃허브) 를 타고 들어가자.

mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);

(https://github.com/firebase/quickstart-android/blob/master/config/app/src/main/java/com/google/samples/quickstart/config/MainActivity.java#L84-L84)


(2) 깃허브 상단에  quickstart-android/config/app/src/main/java/com/google/samples/quickstart/config/MainActivity.java 경로 중 [main]으로 이동한뒤,

   res > xml > [ remote_config_defaults.xml ]까지 가서 xml코드를 복사해올 것이다. 일단 파일명만 복사해오자.


(3) 안드로이드 프로젝트로 돌아와 res폴더에서 우클릭 > [android resource directory ] > * [Resource type :]에서 value-> [xml]로 바꾸어 폴더를 생성하자.

(4) xml폴더에 우클릭하여 new > [xml resource file] 을 선택해 폴더에 똑같은 이름을 [remote_config_defaults.xml]을  복사해서 만들고 코드도 복사해오자.



4. remote_config_defaults.xml 속 key값 value값을 수정한 뒤, 나머지 코드를 완성시키고, onCreate()에서 호출시키자.


(1) 첫번재 키값은 툴바 색깔에 관한 것으로 바꾸자.

(이 키값이 remote_config() 속 ->displayWelcomeMessage()매쏘드 <===> firebase콘솔의 매개변수로 요청하는데 사용된다)

<defaultsMap>

    <entry>

        <key>toolbar_Color</key>

        <value>#000000</value>

    </entry>



(2) firebase공식문서로 다시 돌아와, [작동원리]문서에 3번째인 원격서버에서 값을 가져오는 fetch()요청 리스너 코드를 복사해온다. 성공되었다면 서버와 동기화 된 것이다. 

* 이 fetch()를 RemoteConfig변수에 달아줘야지 <firebase콘솔에서 요청한 값--> 앱으로 가져와 적용한다.>


    remote_config()매쏘드 내부 마지막에 이어서 붙혀넣는다.


mFirebaseRemoteConfig.fetch(cacheExpiration)

        .addOnCompleteListener(this, new OnCompleteListener<Void>() {

            @Override

            public void onComplete(@NonNull Task<Void> task) {

                if (task.isSuccessful()) {

                    Toast.makeText(MainActivity.this, "서버로부터 원격구성 정보를 가져왔습니다.",

                            Toast.LENGTH_SHORT).show();


                    // After config data is successfully fetched, it must be activated before newly fetched

                    // values are returned.

                    mFirebaseRemoteConfig.activateFetched();

                } else {

                    Toast.makeText(MainActivity.this, "서버가 요청했으나 원격구성 정보를 가져오지못하였습니다.",

                            Toast.LENGTH_SHORT).show();

                }

                displayWelcomeMessage();

            }

        });




(3) 빨간줄 오류 중 .fetch()안의 cacheExpiration에다가는 몇초마다 요청할 것인지 적어놓는다. ex> 3600을 적어놓으면 1시간 마다 요청한다.

    우리는 0을 넣어서 앱을 실행할때마다 요청하는 것으로 하자.

        mFirebaseRemoteConfig.fetch(0)


(4) 토스트메세지에 떠있는 context에러는, 나의 메인액티비티 이름이 HomeActivity이므로, main->home으로 바꿔주면 된다.


(4) 다음 빨간줄인  displayWelcomeMessage();의 매쏘드 호출 문구에 대해, 아래에 액티비티 내부 매쏘드를 만들어주자.

(나는 alt+enter로 만들었음. 아직 내부코드는 비워놓음)


    private void displayWelcomeMessage() {

    }


(5) HomeActivity의 onCreate()에 맨마지막에 호출시켜놓자.(아직 코드는 미완성되어있음)


        remote_config();


5. 매칭 시키기 위해서, onCreate()에 멤버변수로 선언되어 있던 Toolbar 변수를 전역변수로 만든다. 

remote_config()가 호출되면  displayWelcomeMessage()에서 받아온 매개변수-value값을 적용시킨다.


(1) displayWelcomeMessage()에서 쓰일 수 있도록, 툴바를 전역변수로 선언해준다.

    private Toolbar toolbar; 


(2) mFirebaseRemoteConfig.변수를 이용해서 get만 치면 <원격서버에서 보낸 매개변수-value>값을 어떤방식으로 불러올지선택할 수 있다. 

    toolbarColor의 value값은 <"#000000"의 String>이므로 getString(" <xml속 키값> "); 으로 불러와서, String 변수에 넣어준다.

   즉, xml속 

<value형> 변수 선언 = mFirebaseRemoteConfig. get<value형> ("키값") 의 형태.  < 키값의 value를 가져온다. >



    private void displayWelcomeMessage() {


        String toolbarColor = mFirebaseRemoteConfig.getString("toolbar_Color");


    }


(3) 똑같이 2개 더 복사한 다음, 수정 안했던 2번째, 3번째 키값을 복사해서, 키값의 자리에 넣어준다. 

   **이때, 2번째 키값에 대한 <value는 false가 적혀있는 것으로 보아, boolean>으로 getBoolean, 해줘야한다. 받는 변수도 boolean으로 선언해주자. 

   **3번째 value는 String 문구다, 2번째가 true일때, 띄울 놈이다.


    private void displayWelcomeMessage() {

String toolbar_Color = mFirebaseRemoteConfig.getString("toolbar_Color");
Boolean welcome_message_boolean = mFirebaseRemoteConfig.getBoolean("welcome_message_boolean");
String welcome_message = mFirebaseRemoteConfig.getString("welcome_message");

    }


(4) 이제 원격서버->remote_config()->displayWelcomeMessage()매쏘드 변수에 받은 값을 이용해서, <툴바색/ 다이얼로그>등을 띄우는 코드를 넣어주자.

이때, boolean에 담긴 정보는 <3번째 키값에 대한 String문구 value값>을 <2번째 키값에 대한 boolean value값>이 true면 띄우고, false면 안띠우는 것이다. 

AlertDialog를 띄울 것이고, boolean의 상태에  따라 띄워지기 위해 if문을 쓴다.

이때, 확인버튼만 넣고, 파라미터로 리스너를 달아서, 3번째 value값인"서버점검중입니다"에 대해 앱을 종료시킨다.


*String의 색("#000000)을 Color.parseColor를 이용해, 배경색으로 지정할 수 있게 하자. 원래 툴바 배경색 지정에서는 int color가 들어가야하는데, String형 #ARGB를 -> int로 바꿔줄 수 있다.

*if(boolean)을 넣으면 참일때만 실행된다.

*AlertDialog는 빌더를 만든 다음 -> 빌더에 문구를 setMessage()하고. 버튼 구성+리스너까지 동시에 달고 ->builder.create().show();로 띄운다.


toolbar.setBackgroundColor(Color.parseColor(toolbar_Color)); //String의 색("#000000)을 Color.parseColor를 이용해, 배경색으로 지정할 수 있게 하자.

if(welcome_message_boolean){
AlertDialog.Builder builder = new AlertDialog.Builder(this);
/*3번째 밸류값인 문구를 을 다이얼로그에 지정해준다.*/
builder.setMessage(welcome_message).setPositiveButton("확인", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

HomeActivity.this.finish(); //중심인 Homeactivity를 닫아서 앱을 종료시킨다.

}
});
builder.create().show();

            

        



6. 앱을 실행하면, 원격요청 받기전에 기존 xml에 저장된 #000000(검은색)으로 툴바가 색칠해져있다. 서버에 요청을 했는데, 만약 매칭되는 매개변수가 앱에 없으면, 그때 띄워줄 녀석이 만들었던 xml이다.

콘솔에서 원격조정->remote_config()->displayWelcomMessage() 을 통해 앱에 적용 시켜보자.

(1) firebase콘솔로 가서 Remote_config로 와서 [매개변수 추가]를 눌러서  [매개변수_키]에는 <만든 xml에서의 key값> / [기본값]에는 <바꿀value값>을 넣어주자.

(2) 툴바는 #000000의 검은색으로 되어있었으니까 , #ffffff의 흰색으로 줘보고,

다이얼로그는 true로 해서 띄워볼 예정이다.

문구도 바꾸고 싶은대로 바꿔보자. 따옴표 없어도 된다.

(3) 우측 상단의 [변경사항 게시]를 눌러 적용시키자

(4) 앱을 껐다키면 바로 적용되는 것을 알 수 있다.




7. 이제 value값만 바꿔서 적용시킬 수 있다.





앱이 꺼졌을 때 Reporting 하는 부분에 대해서 공부해보자. 오류로 앱이 꺼진 뒤, [ 3분 ] 정도의 시간이 지나야 Reporting을 확인할 수 있다고 한다.


[1] Crash Reporting 대신 Crashlytics가 사용될 예정이라고 한다. 일단은 문서로 가서 예전버전인 Crash Report를 사용해보자.


1. Firebase사이트 문서 > android시작하기 > 오류보고 > android >[오류보고]로 온다

(1) [오류보고설정]에서 필요한 라이브러리를 gradle 모듈수준에서 컴파일 해준다. *이전 파이어베이스 라이브러리들과 버전을 맞춰준다.

 - compile 'com.google.firebase:firebase-crash:11.4.2'


2. 크러쉬를 강제적으로 날린다. 플루팅 액션버튼 fab의 클릭리스너를 클릭하여, xml도 없고 && id를 참조안한 editText를 setText해서 호출해서 오류를 낼 것이다.

(1) HomeActivity의 전역변수로 editText를 선언하고 findViewByid를 안해준다.

    private EditText error;

(2) HomeActivity 중 플로팅액션버튼 fab의 클릭리스너에서 안에 내용물을 지우고, error.에다가 setText해주자.

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

        fab.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                error.setText("eee");

            }

        });

(3) 앱을 실행해서, 플루팅액션버튼을 눌러서 에러를 내자.

(4) 이제 Firebase콘솔의 왼쪽메뉴 STABILITY> [Crash Reporting ] 으로 들어가면, "방금 오류가 발생했습니다"라고 곧 표시될 거라는 문구가 뜬다.



(5) 기다리면 문서가 업데이트 된다. 나는 3분이상 기다렸다. 아래 그림과 같이 클릭하면 더 자세하게 나온다.




3. 공식문서에서 소개하는 코드를 이용해서, 오류내는 코드에다가 try/catch문으로 감싸준 뒤, FirebaseCrash.report(e) 코드를 catch문에 넣어서 firebase에 보고해보자.

(실제로는 이렇게 사용하진 않는다)

*단축키 [ Ctrl+alt+T ]로 해당 문장을 try/catch로 감싸줄 수 있다. 

(1) 이제 오류가 발생할 문장을 try/catch로 잡은 뒤, catch문 안에다가FirebaseCrash.report(e);매쏘드를 호출하여 앱이 꺼지지 않고도 보고 되도록해보자.

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

        fab.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {


                try {

                    error.setText("eee");

                } catch (Exception e) {


                    FirebaseCrash.report(e);


                }

            }

        });


[2] 앱을 출시할때는 ProGuard(암호화)를 통해야하는데, 이럴 경우 크래쉬 리포트에 HomeActivity 등 이름들이 이상하게 표시된다. 이럴 경우 필요한 것이 ProGuard 매핑이다.


android > [오류보고] 문서 전체에  ProGuard 매핑(암호화 풀기) 방법 2가지가 나와있다.


1. ProGuard 매핑 파일 직접 업로드 - 스마트한 방법이 아니라 < 비추천 >

(1)[ProGuard 매핑 파일 직접 업로드] 문서에 보면, 경로가 나와있다.  

 - app/build/outputs/mapping/debug/app-proguard-mapping.txt 라는 경로를 직접 따라간다.


(2)  firebase콘솔 > Crash Reporting > [매핑파일] 탭으로 가서 직접 업로드 해준다.


2. gradle로 편하게 ProGuard 맵핑하기

(문서경로 : https://firebase.google.com/docs/crash/android?authuser=0)


(1) [Proguard 라벨 해독]에 있는 아래 코드를  복사한 뒤에, 2번째 gradle인 <build.gradle(Module수준)>으로 가져와야한다. 

    minifyEnabled를 true로 해주어, 디버그시 프로가드를 작동시킬 것인지 물어보는 대목이다.


debug {

   minifyEnabled true

   proguardFiles getDefaultProguardFile('proguard-android.txt'),

     'proguard-rules.pro'

}


(2) 위 코드를 2번째 gradle의 buildType{}아래 수준, 즉, release{}와 같은 수준으로 debug{}를 넣어준다. 

이때, release{}의 프로가드 작동 물음인 minifyEnabled를 true로 바꿔준다.

<릴리즈 할때, 프로가드로 암호화하고, 디버그시도 프로가드로 암호화 >하게 된다.


    buildTypes {

        debug {

            minifyEnabled true

            proguardFiles getDefaultProguardFile('proguard-android.txt'),

                    'proguard-rules.pro'

        }

        release {

            minifyEnabled true

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }


(3)[Gradle로 ProGuard 매핑 파일 업로드]문서에서, firebase플러그인을 사용하여 ProGuard를 <업로드>하는 코드를 가져온다.

즉, firebase 맵핑 작업을 firebase콘솔에 업로드 해주는 코드인데, 

<첫번째 gradle(프로젝트 수준)>에서  플러그인 경로을 추가해주어야 한다. 이때는,buildscript {} 아래 dependencies{}에 추가해주어야한다.


buildscript {

    repositories {

        jcenter()

        mavenCentral()

        google()


    }

    dependencies {

        classpath 'com.android.tools.build:gradle:3.0.1'

        classpath 'com.google.gms:google-services:3.1.0'

        

        classpath 'com.google.firebase:firebase-plugins:1.1.1' //파이버베이스에 ProGuard맵핑 한것을 업로드 시키는 플러그인

        

    }

}


(4) 이제 경로를 추가하였으니 실제 앱에 플러그인을 적용시키는 것을 <두번째 gradle(app 모듈수준)>의 첫번째 플러그인 적용 밑에 넣어준다. 다 완료되면 sync now 한다.


apply plugin: 'com.android.application'

apply plugin: 'com.google.firebase.firebase-crash' //파이어베이스 프로가드 맵핑 플러그인 적용


android {

    compileSdkVersion 27

    buildToolsVersion "27.0.3"

    defaultConfig {

..

(5) [Gradle로 ProGuard 매핑 파일 업로드]로 gradle을 이용한 프로가드 맵핑 오류보고 받기 위해서는, 문서의 3번째 과정인, [ 비공개 키를 생성해서 인증키 ]를 받아야한다. 

- 해당 프로젝트의 콘솔로 가서 좌측상단의 [ 설정 ] 아이콘을 눌러 > 설정 > [서비스계정] 탭 > [비정상 종료 보고 ]에 들어가서  [새 비공개 키 생성]을 클릭한다.

- [프로젝트명 + 블라블라.json ]파일이 다운로드 된다. 문서 4번째 과정을 살펴보자


 <1> 안드로이트 프로젝트의 <gradle.properties>로 가서, FirebaseServiceAccountFilePath 라는 속성을 복붙 한뒤, = 등호를 이용해서 경로를 지정해준다.

org.gradle.jvmargs=-Xmx1536m

FirebaseServiceAccountFilePath = 

 <2> 윈도우는 다운로드 폴더를 지정해줘야한다. 

*다운로드 폴더를 열고 , json파일을 cmd창에 올리면, 파일경로가 나온다. 그것을 앞에서부터 shift로 복사해서 등호 우측에 붙혀주자.

*이때, 폴더 경로의  \ 를 /로 고쳐주자.


# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
FirebaseServiceAccountFilePath =c:/Users/cho/Downloads/chojaeseong-12b71-firebase-crashreporting-8jspg-fe27f507fd.json

# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true


(6) 이제 5번과정의 ./gradlew :app:firebaseUploadReleaseProguardMapping 를 복사해서 [Terminal]탭에서, 맨 앞의 " ./ " 부분은지우고 실행시키면 작동된다. gradlew :app:firebaseUploadReleaseProguardMapping 만 복사해서 터미널에. 붙혀주자

* 작동이 안되는 사람은 모듈수준 gradle에서 realse와, debug가 모두 true인지 확인하자.

* 나같은 경우, app: 을 찾을 수 없다는 오류가 떴다.** <개고생함>

  <1> 구글문서에 보니, adroid 3.0으로 업데이트 한 사람은 플러그인 버전을 1.1.1-> 1.1.5로 바꾸라고 나와있다!!!

classpath 'com.google.firebase:firebase-plugins:1.1.5' //파이버베이스에 ProGuard맵핑 한것을 업로드 시키는 플러그인 경로

  <2> 그래도 안되길래, gradlew clean init build 을 발견해서, 초기화했다. -> 오류가 뜨면서 gradle.build<모듈>에 android{} 안쪽에 아래 문장을 달아주라고 했다. 

    lintOptions {

        abortOnError false

    }

android {
compileSdkVersion 27
buildToolsVersion "27.0.3"
defaultConfig {
applicationId "com.example.cho.firebase"
minSdkVersion 15
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}

  <3> 완료. 콘솔에서 확인해보았더니  BUILD SUCCESSFUL in 11s 프로가드 매핑파일이 정상적으로 올라갔다.


- 이제 프로가드를 쓰더라도 정상적으로, 어디서 오류났는지 확인이 가능하다.

- 디버그는 매핑을 안해주고, 나중에 릴리즈 했을 때, 매핑해준다.




1. 갤러리를 갔다가 + < 이미지 까지 선택되어 data가 null이 아닐경우>로 조건을 하나 더 추가해서 그 경우만 이미지 경로를 imagePath에 담고 이미지뷰에 뿌리도록 하고,  갤러리를 갔다가 +  이미지를 선택안해서 data가 null일 경우는 이미지를 선택하세요. 라는 문구를 띄워준다.


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { //38. 갤러리에서 선택된 이미지 정보를 액티비티로 받아올 온액티비티리절트 오버라이딩
super.onActivityResult(requestCode, resultCode, data);

if(requestCode == GALLERY_CODE && data!=null){ // && data!=null 과 else문을 통해서 이미지 선택안되도 선택되도록 띄우라고 만들었다!
//System.out.println(data.getData());
//System.out.println(getPath(data.getData()));//40. 그냥 인텐트를 받은 것과, 인텐트를 이미지 경로로 바꿔줄 매쏘드를 이용한 것의 차이점을 보기 위한 테스트

//52. 얻어온 이미지 경로를 파일객체에 바로 담지말고, 51.에서 선언한 전역변수 String에 한번 담은후, string변수를 f에 넘겨, 저장되도록 하기
imagePath = getPath(data.getData());


//47. 파일객체에 갤러리선택이미지의 경로 담기 -> 52번에서 String imagePath에 저장후 담아주기로 바꿈
File f = new File( imagePath );
//50. file객체를 imageview에 뿌려주는 방법
imageView.setImageURI(Uri.fromFile(f));
}else if(requestCode == GALLERY_CODE && data == null) {
Toast.makeText(getApplicationContext(), "업로드할 이미지을 먼저 선택하세요.", Toast.LENGTH_SHORT).show();
}
}

2. HomeActivity의 upload()매쏘드에서, 이미지를 선택하여 Storage에 업로드 시키는 부분의 파라미터 uri가 null이 아닐때 작동하도록 if문을 달고 

   else로 넘어오는 이미지경로가 null 인경우는 imageDTO의 imageUrl, imageName에다가 default를 대입해서 넘긴다.


public void upload(String uri){ //46. 4345코드는 onActionResult()에 있어서, 갤러리에서 받은 정보를, 갤러리꺼지고 액티비티에서 바로 받아서 업로드 시켰던 것을===> upload()를 만들어 옮긴다.

//45. 빨간줄 난 storageRef를 위해 복붙해온 코드
StorageReference storageRef = storage.getReference();

if(uri!=null) {

//43. 공식문서에서 로컬파일 업로드부분을 복사해온다.
final Uri file = Uri.fromFile(new File(uri)); //44. 복사코드 중에 getPath(data.getData())를 넣어주는 곳-> 옮겨오면서, 파라미터의 uri경로가 들어간다.
StorageReference riversRef = storageRef.child("images/" + file.getLastPathSegment());
UploadTask uploadTask = riversRef.putFile(file);

// Register observers to listen for when the download is done or if it fails
uploadTask.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
// Handle unsuccessful uploads
}
}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
@Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {

Uri downloadUrl = taskSnapshot.getDownloadUrl(); // 업로드 된 이미지의 다운로드 주소를 의미한다. 공식문서에서 자동으로 생성되어있다.

//57.setValue에 넘겨줄 데이터구조ImageDTO의 객체를 만들어, 정보를 담아준다
ImageDTO imageDTO = new ImageDTO();
imageDTO.imageUrl = downloadUrl.toString();
imageDTO.title = title.getText().toString();
imageDTO.description = description.getText().toString();
imageDTO.uid = auth.getCurrentUser().getUid();
imageDTO.userid = auth.getCurrentUser().getEmail();

imageDTO.imageName = file.getLastPathSegment(); //storage삭제를 위해, imageDTO를 database에 올릴 때, 파일명도 같이 보낸다.

//56. 사진 업로드 성공시, 데이터베이스에도 정보를 넘겨주는 코드 //58. push.도 넣어서 싸이즈 안나게 하기
database.getReference().child("images").push().setValue(imageDTO);

//내가 만든 것
Toast.makeText(getApplicationContext(), "Storage에 사진업로드와, Datebase에 storage상 사진주소, 제목, 타이틀 이 저장되었습니다.", LENGTH_SHORT).show();

}
});
}else{

ImageDTO imageDTO = new ImageDTO();
imageDTO.title = title.getText().toString();
imageDTO.description = description.getText().toString();
imageDTO.uid = auth.getCurrentUser().getUid();
imageDTO.userid = auth.getCurrentUser().getEmail();

imageDTO.imageUrl = "default";
imageDTO.imageName = "default";

database.getReference().child("images").push().setValue(imageDTO);


Toast.makeText(getApplicationContext(), "사진없이 게시물이 저장되었습니다.", LENGTH_SHORT).show();

}



}



3. BoardActivity delete_content()매쏘드에서는 storage파일삭제-> database삭제 하기 전에 사진 없이 올렸을 땐, storage가 없으므로, 

imageDTOs 리스트에 imageName 또는 imageUrl이 "default"로 지정된 경우에 한하여 storage삭제 없이 database만 삭제하도록 먼저 if문을 준다.

private void delete_content(final int position){

if(imageDTOs.get(position).imageName.equals("default") || imageDTOs.get(position).imageUrl.equals("default") ) {

database.getReference().child("images").child(uidLists.get(position)).removeValue().addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {

Toast.makeText(BoardActivity.this, "DB 삭제 성공", Toast.LENGTH_SHORT).show();

}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {

Toast.makeText(BoardActivity.this, "DB 삭제 실패", Toast.LENGTH_SHORT).show();

}
});
}else {

storage.getReference().child("images").child(imageDTOs.get(position).imageName).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {

Toast.makeText(BoardActivity.this, "사진삭제 완료", Toast.LENGTH_LONG).show();


// 사진삭제 성공후에, db>images>키값을 지운다.
database.getReference().child("images").child(uidLists.get(position)).removeValue().addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {

Toast.makeText(BoardActivity.this, "DB 삭제 성공", Toast.LENGTH_SHORT).show();

}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {

Toast.makeText(BoardActivity.this, "DB 삭제 실패", Toast.LENGTH_SHORT).show();

}
});


}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(BoardActivity.this, "사진삭제 실패", Toast.LENGTH_LONG).show();

}
});
}




}



이전시간 ) 게시글 삭제버튼 누르면 Storage에 있는 이미지가 삭제되도록 코드를 구성해본다.

오늘은 게시글 삭제버튼을 눌렀을 때, Storage이미지와 함께, Database의 정보들도 사라지도록 하게 한다.


[1] 게시물 정보(in Database)까지 삭제하기


1. BoardActivity의 어댑터 이너매쏘드 delete_content()에서 , 이전시간에 작성한 storage에 이미지파일 삭제 코드를  잠시 주석처리하자

* db가 정상적으로 날아가는지 확인되면, 통합할 것이다.

* 주석처리는 코드 선택 후 [ Ctrl + '/' ]를 통해 한번에 주석처리할 수 있다.


2. BoardActivity의 어댑터 이너매쏘드 delete_content()에서, database에 키값까지 접근하여, removeValue()를 통해서 삭제한다.

(storage는 delete()로 삭제했다)


(1) database객체를 이용해, 레퍼런스로 접근하여. images > 해당 게시물 키값으로 간다음, removeValue().를 통해 삭제한다. 삭제성공유/무에 따른 리스너인 addOnSucceesListener도 달아준다. 사실, 디비 삭제에 있어서 fail날 확률이 아주 적지만, fail리스너도 달아준다. 그리고 토스트메세지도 하나 띄워준다.

( 이때, removeValue() 대신 setValue(null)을 넣어서 삭제해도 가능하긴 하다)

(토스트 메세지를 적을 때, Toast까지 적은 뒤 선택창에서 creat new a Toast를 [TAB]키로 완성시키면, 자동완성되어 편리하다)


            database.getReference().child("images").child("디비 키값").removeValue().addOnSuccessListener(new OnSuccessListener<Void>() {

                @Override

                public void onSuccess(Void aVoid) {


                    Toast.makeText(BoardActivity.this, "삭제 성공", Toast.LENGTH_SHORT).show();


                }

            }).addOnFailureListener(new OnFailureListener() {

                @Override

                public void onFailure(@NonNull Exception e) {


                    Toast.makeText(BoardActivity.this, "삭제 실패", Toast.LENGTH_SHORT).show();

                    

                }


(2) 이제 "디비 키값"을 가져와야한다. 키값의 사용은

onCreate()에서, database의 images폴더에 달아준, addValueEventListener()의 onDataChange()안에서, database의 키값들을 리스트 uidList에 [키값 명]들을 받아넣었었다.

onBindViewholder에서 onStarClicked()의 파라미터로, database에서 키값을 받아온 리스트 uidList.에서 i번째 get(position)를 사용한 적이 있는데,

공식문서의 onStarClicked()와는 다르게, 파라미터로 데이터베이스까지 접근해서 가져올 필요가 없다. 키값명들은 uidList에 이미 저장되어있기 때문이다.

그것을 가져와 넣어준다.  

즉, "디비 각 게시물 키값" = uidLists.get(position)


(3) 앱을 실행해서, 정상적으로 키값이 삭제되는지 확인하자.


해석)

imageDTOs  => Database > images > 키값(각 게시물) > values를 받아오는 리스트 by (for문의 각 snapshot에서)getValue( DTO.class) => DTOs 리스트

uidLists  => Database > images > 키값(각 게시물)을 받아오는 리스트(values들은 하위값으로서 키값으로 통제가능) by getKey()=> String 리스트

delete_content() 내부에서 String 리스트 uidLists.get(position)을 통해 선택된 i번째 키값을 삭제하여, value들까지 다 삭제시킨다.


3. Storage 삭제후 -> Database 키값 삭제순으로 프로그래밍 하자. 

storage이미지를 삭제하는데 있어서, imageDTOs.get(position).imageName를 사용했다. 

만약 db부터 지운다면, db의 내용이 바뀌므로 [옵저버패턴]으로 인해서, onDataChanged호출로, position이 바뀌어버린다.

그렇기 때문에, Storage먼저 삭제하고, 그것이 성공하면 그 db도 지우도록 한다.


(1) 콜백방식을 이용한다. 일단 1.에서 했던 주석처리를 풀자. 

(2) Storage의 onSuccess 안에다가 db를 지우는 코드 전체를 넣자. 파라미터 position이 빨간줄 나면, alt+enter로 파라미터 란에 final을 달아주자


private void delete_content(final int position){


            storage.getReference().child("images").child(imageDTOs.get(position).imageName).delete().addOnSuccessListener(new OnSuccessListener<Void>() {

                @Override

                public void onSuccess(Void aVoid) {


                    Toast.makeText(BoardActivity.this, "사진삭제 완료", Toast.LENGTH_LONG).show();


                    

                    

                    // 사진삭제 성공후에, db>images>키값을 지운다.

                    database.getReference().child("images").child(uidLists.get(position)).removeValue().addOnSuccessListener(new OnSuccessListener<Void>() {

                        @Override

                        public void onSuccess(Void aVoid) {


                            Toast.makeText(BoardActivity.this, "DB 삭제 성공", Toast.LENGTH_SHORT).show();


                        }

                    }).addOnFailureListener(new OnFailureListener() {

                        @Override

                        public void onFailure(@NonNull Exception e) {


                            Toast.makeText(BoardActivity.this, "DB 삭제 실패", Toast.LENGTH_SHORT).show();


                        }

                    });

                    

                    


                }

            }).addOnFailureListener(new OnFailureListener() {

                @Override

                public void onFailure(@NonNull Exception e) {

                    Toast.makeText(BoardActivity.this, "사진삭제 실패", Toast.LENGTH_LONG).show();


                }

            });




        }




  1. 2020.10.17 18:44

    비밀댓글입니다

  2. ds1kox 2020.11.17 17:29

    도움되는 글 잘 배우고 가용~

게시글 삭제버튼 누르면 Storage에 있는 이미지가 삭제되도록 코드를 구성해본다.


[1] 게시글 삭제에 있어서, 이미지 파일명을 받을 변수가 필요하다.

(Database에 있는 정보들은 바로 삭제할 수 있지만, Storage에 있는 이미지를 같이 지울려면 파일명이 필요하다)


1. 이미지 삭제를 위해, 삭제할 이미지파일명이 필요로 한다. ImageDTO.java에서 그것을 저장할 String 변수를  imageDTO클래스에 새롭게 추가한다.

    public String imageName;


2. HomeActivity.java의 upload()매쏘드의 코드 중 [갤러리에서 선택된 그림을 Storage에 업로드시키는 그림파일 이름]을 ->

데이터베이스 에 정보를 올리는 addOnSuccessListener()에서, 새로만든 imageDTO.imageName에 담아야 -> firebase콘솔에도 저장된다.

(1) upload()매쏘드 속 갤러리에서 받아온 uri->file객체->file.getLastPathSegment()를 통해 이미지 파일명을 image폴더에 업로드 하는 것을 알 수 있다.,     StorageReference riversRef = storageRef.child("images/"+file.getLastPathSegment()); 코드 중에,

<   file.getLastPathSegment()  >가 갤러리에서 선택된 이미지 파일명이다. 이 작업을 통해 Storage에 업로드 된다.


(2)addOnSuccessListener 의 onSuccess()매쏘드에서는, Storage에 이미지가 성공적으로 업로드 될 때, 관련 Database에 정보를 쌓는 일을 하였다.

여기서는 복사한 파일명을 imageDTO.imageName에 담아서 Database에 업로드 시키도록 한다.

*이때 file변수에 빨간줄이 뜬다. [alt+enter]로 내부리스너에서도 쓰일 수 있도록 final을 붙혀준다.


imageDTO.imageName = file.getLastPathSegment(); //imageDTO를 database에 올릴 때, 파일명도 같이 보낸다.


3. BoardActivity에서 어댑터의 이너함수로 게시물 삭제매쏘드 delete_content()를 만든다. 

onBindViewHolder에서 position을 이용해, database->imageDTOs를 받은 상태에서, 파일명을 의미하는 i번째, .getName을 받아서 그 파일을 삭제한다.

*(이것도 i번재 position을 잡아 삭제하기 때문에, 어댑터 이너함수로 만든다. 

onStarClicked()도 공식문서에서 긁어온 매쏘드지만, i번째 position잡아서 쓰는 것이므로, 어댑터의 이너함수로 만들었었다.)

(1)(onStarClicked() 밑에) private void  delete_content(){} 를 만든다.

        private void delete_content(){


(2) 콘솔의 storage의 이미지를 삭제하므로, Storage에 접근해야한다. BoardActivity 전역변수로 FirebaseStorage 변수를 추가해주고, 

onCreate()에서 싱글톤 패턴으로 불러온다.

private FirebaseStorage storage;

...

        storage = FirebaseStorage.getInstance();



(3) delete_content()안에 storage객체를 이용해서 지우는 코드를 넣어준다.(*child는 경로 한단계 아래를 의미한다)

 - storage.getReference().child("images").child("파일명").delete();까지만 해도 삭제가 되지만

 - 확인(콜백)을 받기위해서는 썩세스 리스너를 달아준다.(addOnS으로 시작하고 파라미터는 new On까지 쳐서 자동완성시킨다)

 - 이왕 다는 김에 addOnF ( new On~)으로 실패리스너도 달아주고, 토스트메세지를 넣어준다.

 


        private void delete_content(){


            storage.getReference().child("images").child("파일명").delete().addOnSuccessListener(new OnSuccessListener<Void>() {

                @Override

                public void onSuccess(Void aVoid) {


                    Toast.makeText(BoardActivity.this, "삭제 완료", Toast.LENGTH_LONG).show();


                }

            }).addOnFailureListener(new OnFailureListener() {

                @Override

                public void onFailure(@NonNull Exception e) {

                    Toast.makeText(BoardActivity.this, "삭제 실패", Toast.LENGTH_LONG).show();


                }

            });


        }



****(4) 파일명은 item의 position에 따라 다르다. 이것은 position이 쓰이는 onBindViewHolder에서 파라미터로 받아야한다. 

긴 삭제 코드를 매쏘드 내부에 넣고, 파라미터는 position만 전달해주도록 만든다. 

database에서 받은 리스트 imageDTOs에서 get(position)한 뒤, 거기서 imageName을 빼냄.

(onStarClicked에서는 파라미터로 position만 받지 안았음. 공식문서에서 긁어온 매쏘드이라서, 내부에 긴 코드를 적는 것이 아니라, 파라미터로 긴 코드를 보내야만 한다. 정의된 매쏘드가 구조상 어쩔 수 없었음.)


        private void delete_content(int position){


            storage.getReference().child("images").child(imageDTOs.get(position).imageName).delete().addOnSuccessListener(new OnSuccessListener<Void>() {


4. 이제 각 item화면에 뿌려질 휴지통이 필요하다.

(1) https://material.io/icons/ 에서 delete를 검색해서 휴지통을 받고, res폴더로 직접가서 android용 폴더5개를 모두 복붙해준다.

(2) item_board.xml로 이동해서, 이미지뷰를 추가해준다. id는 item_deleteButton_imageView로 준다.

(이미지버튼으로 주고, 그림을 백그라운드로 줘도 상관없지만, 이미지뷰로 넣고 클릭이벤트로 주는 것이 가장 깔끔)

(3) BoardActivity로 돌아와, 어댑터의 커스텀뷰홀더에 추가 및 id참조를 해준다.

(4) onBindViewHolder로 가서 setOnClickListener를 달아준 뒤, position을 넣어서, delete_content()를 호출해준다.

            ((CustomViewHolder)holder).deleteButton.setOnClickListener(new View.OnClickListener() {

                @Override

                public void onClick(View v) {

                    delete_content(position);

                }

            });


5. 이제 앱을 실행하기전에, imageName 양식을 안갖추어진 데이터베이스는 모두 삭제해준 다음, 새로 업로드한 뒤, 삭제를 테스트해보자.

*Storage는 실시간이 아니라서, 다시 클릭해서 확인해야 삭제된 것을 확인할 수 있다.

*아직 데이터베이스는 안지우기 때문에 남아있으니, 다음시간에 완성할 것이다.




데이터베이스에 있어서 많이 쓰이는 용어로, 한 데이터를 입력하는 동안 다른데이터는 입력되지 못하게 하는 입력방식

동시에 좋아요 버튼을 누를 때, 손상이 생길 수도 있으니, 트랜잭션은 좋아요버튼을 구현할 때, 많이 쓰인다.

아래는 공식문서 설명이다.

데이터를 트랜잭션으로 저장

증가 카운터와 같이 동시 수정으로 인해 손상될 수 있는 데이터를 다루는 경우 트랜잭션 작업을 사용할 수 있습니다. 이 작업에 지정하는 두 인수는 업데이트 함수 및 선택적 완료 콜백입니다. 업데이트 함수는 데이터의 현재 상태를 인수로 취하고 이 데이터에 새로 기록하려는 값을 반환합니다. 새 값이 기록되기 전에 다른 클라이언트에서 해당 위치에 기록하면 업데이트 함수가 새로운 현재 값으로 다시 호출되고 쓰기가 다시 시도됩니다.

예를 들어 소셜 블로깅 앱에서는 다음과 같이 사용자가 글에 별표를 주거나 삭제할 수 있고 글이 별표를 몇 개 받았는지 집계할 수 있습니다.


[1] 좋아요 버튼으로 쓰일 그림 다운 받기

1. 먼저 material.io에 접속해서 icon중 favorite를 검색하여 좋아요 버튼을 찾아 png로 다운받는다.

2. 안드로이드 res에서 우클릭> Directory Path > res폴더로 이동해서, 다운받은 android 해상도별 폴더(5개)를 다 복사해서 붙혀넣는다.

- 그러면 각 폴더마다, 2개의 png들이 차있게 된다. 다른 이미지가 필요할 때 똑같이 해주면, 해상도별 폴더에 추가된다.

**drawable 폴더가 아니라 res폴더 밑에 5개 폴더 다 복사해넣기


[2] 안드로이드 프로젝트에서 트랜잭션 구현하기

1. 각 item화면인 item_board.xml로 가서 이미지뷰를 하나 추가해주고, 배경 drawable로 경계만 있는 하트를 지정해준다. id는 item_starButton_imageView라고 지정해준다.

2. 어댑터의 CustomViewHolder에서는 변수(starButton)선언 및 생성자안에서 id참조해주고, 이제 onBindViewHolder에서  뷰들의 이벤트만 넣어 된다

3. 트랜젝션 코드를 firebase콘솔 공식문서에서 가져와야한다.


(1) 문서로 이동 > 안드로이드 시작하기 > 실시간 데이터베이스 > [데이터 읽기 및 쓰기] (교재에선 '데이터 저장'이었다) > 

[데이터를 트랜잭션으로 저장] 문서에서(경로 : https://firebase.google.com/docs/database/android/read-and-write?hl=ko)


private void onStarClicked(DatabaseReference postRef) {

    postRef.runTransaction(new Transaction.Handler() {

        @Override

        public Transaction.Result doTransaction(MutableData mutableData) {

            Post p = mutableData.getValue(Post.class);

            if (p == null) {

                return Transaction.success(mutableData);

            }


            if (p.stars.containsKey(getUid())) {

                // Unstar the post and remove self from stars

                p.starCount = p.starCount - 1;

                p.stars.remove(getUid());

            } else {

                // Star the post and add self to stars

                p.starCount = p.starCount + 1;

                p.stars.put(getUid(), true);

            }


            // Set value and report transaction success

            mutableData.setValue(p);

            return Transaction.success(mutableData);

        }


        @Override

        public void onComplete(DatabaseError databaseError, boolean b,

                               DataSnapshot dataSnapshot) {

            // Transaction completed

            Log.d(TAG, "postTransaction:onComplete:" + databaseError);

        }

    });

}


(2) 복사한 코드인 onStarClicked()매쏘드를 <어댑터 내부에 이너 매쏘드>로 가져온다.( 교재에선 getItemCount 밑/ 커스텀뷰홀더 위)

my)어댑터 내부로 가져오는 까닭은, 어댑터에서 onBindViewHolder에서 이미지뷰에 클릭리스너를 달아 호출할 것이기 때문


(3) 빨간줄 나는 오류를 잡기 위해서, 해당 문서에서 올리다 보면 [데이터 업데이트 또는 삭제] > [특정 필드 업데이트] 문서에 DTO를 Post라고 만들어놓았다. 우리가 필요한 것은 아래 2줄이다. 우리가 만든 ImageDTO안에 집어넣어준다.


   public int starCount = 0;

    public Map<String, Boolean> stars = new HashMap<>();


cf)

Hashmap이란?

map 인터페이스의 종류로 Key,Value값으로 데이터를 저장하는 형태이다. Hashmap은 Key값을 중복되지 않고, value값은 중복이 된다. 

리스트와의 차이점은, 순서는 상관안쓴다는 점.

ex>           Key, value 

map.put("엄마","10")

map.put("아빠","10")

map.put("","20")  // 엄마 key에 저장된 value는 나중에 입력된 key의 value값이 입력된다.

Key는 유일해야 하고, value값은 같은 값이 여러개 있어도 괜찮다.

Map은?

키(Key),값(Vlaue)을 하나의 쌍으로 묶어서 저장하는 컬렉션 클래스들을 구형하는데 사용하는 형태이다.



(4) 공식문서의 DTO는 class Post p로 되어있다. 이것들을 우리의 ImageDTO imageDTO로 바꿔주는 작업을 통해 빨간줄을 다 해결한다.

(5) getUid()는, 액티비티의 전역변수로 private FirebaseAuth auth;를 만들고 onCreate()에서 인스턴스를 받아서, auth.getCurrentUser().를 붙혀주면된다.

    (+)private FirebaseAuth auth; /(+) auth = FirebaseAuth.getInstance(); / getUid() -> auth.getCurrentUser().getUid()


간단하게 코드를 보자면, 

좋아요 버튼을 눌러서, onStarClicked()가 호출되는 시점에서 받은 DTO변수안의 필드 .stars라는 해쉬맵<String, boolean>변수 안에. 좋아요 누른 사람들들의 Uid가 배열로 담기는데, 클릭한 상태에서 Uid키에 포함되어 있다면 이미 누른 사람이므로 => starCount는 -1 해주고, stars해쉬맵안에 있던 Uid를 remove()해버린다.만약 누른사람이 Uid배열에 안에 포함되지 않은 신규이면 => starCount는 +1해주고, stars해쉬맵에  true로서, Uid를 put()해준다.


4. 이제 어댑터의 onBindViewHolder속에서 커스팅뷰홀더로 캐스팅한 holder를 가지고, 이미지뷰에 클릭리스너를 달아준 뒤, onStarClicked를 넣어준다. 

(1)onStarClicked()의 파라미터로서, (DatabaseReference postRef)라고 적혀있는 것은 데이터를 읽고 쓸때 필요한 변수이다. 우리는 이 databaseReference변수를 FirebaseDatabase database.를 이용한 database.getReference()로 바로 쓸 수 있다. 

 *여기서 파라미터로 접근해야하는 것은 [ i번째의 데이터베이스 > images 폴더 > 각 게시물을 의미하는 키값 ]까지 접근해야한다.

( *온스타클릭드()의 파라미터는 폴더를 지나서 각 개별 키값을 파라미터로 넣어줘야 하는구나!)

 1) i번째의 키값을 알려면, 전체 키값을 받아야한다. snapshot을 이용해 for문 안에서image폴더의 키값 속 value들을 받았던 곳에서, 

   키값도 전체를 따로 받아줄 것이다.

 2) 그리고 이전시간에 선언했던 이전시간에 private List<String> uidLists new ArrayList<>(); 안에다가, 받은 키값을 모두 리스트로 대입해 줄 것이다

 3) position을 사용할 수 있는 onBindViewHolder에서, onStarClick()속 파라미터에, i번째 키값을 uidList.get(position)으로 넣어줄 것이다. image폴더까지의 접근은, onCreate()에 애드밸류이벤트리스너를 달아줄 때 사용했던 데이터베이스 참조 문장 중 database.getReference().child("images").까지만 복사해서 가져올 것이다.


(2) firebase콘솔 > 데이터베이스 > images에 보이는 복잡한 잡영어의 게시물들의 정보(키값)를 알아내야한다.

이때, 잡영어 = 키값 , 안에 내용물(title, imageUrl 등등)은 value라 한다.

(3) 각 게시물을 의미하는 키값정보들은 addValueEventListener의 onDataChange()에서 dataSnapshot으로 넘어오기 때문에

for()문 안에서 

snapshot.getValue(ImageDTO.class); 한 것 밑에

snapshot.getKey();를 통해 가져와 String uidKey 에 담는다( 키값은 String이다). for문을 통과하면서 전체 키값을 다 담는다.

(4) 그리고 imageDTO리스트인 imageDTOs를 선언할 때, 같이 선언한 private List<String> uidLists = new ArrayList<>();에다가 for문을 도는 동안 다 담아준다.

*아! value값이랑 key값이랑 따로 받아가져오는 구나.

(6) 키값이 데이터바뀌어 호출될때마다 쌓이므로, imageDTOs처럼 for문 돌기전에 clear()도 해주어야한다.


                imageDTOs.clear();

                uidLists.clear();

                for(DataSnapshot snapshot : dataSnapshot.getChildren()){

                    ImageDTO imageDTO = snapshot.getValue(ImageDTO.class);

                    String uidKey = snapshot.getKey(); //onStarClicked()를 위해, 게시물의 역할인 키값 가져오기


                    imageDTOs.add(imageDTO);

                    uidLists.add(uidKey); //onStarClicked()를 위해, 받아온 키 값을 어댑터에서 사용가능한 리스트로 받아놓기

                }

(7) 이제 이미지뷰 클릭리스너에 있는 onStarClicked()의 파라미터에, 앞에서 images폴더까지의 경로에다가 추가로 .child(uidLists.get(position))을 통해서,

i번째 아이템이 클릭되었을 때, 받아온 전체 데이터 중 의 i번째 키값(게시물)에 해당하는 내용을 넘겨준다. 

*아! 키값 전체를 받아와서, i번째 키값을 이용하는 것은, 데이터베이스>폴더>키값 접근시, 단순한 [키값 명]을 얻기 위함이구나.

            ((CustomViewHolder)holder).starButton.setOnClickListener(new View.OnClickListener() {

                @Override

                public void onClick(View v) {


                    onStarClicked(database.getReference().child("images").child(uidLists.get(position)));

                }

            });


[3] 좋아요 버튼을 눌렀을 때, 좋아요 이미지로/ 아니면 테투리만 있는 이미지로 바뀌어야한다. 

(클릭리스너 -> setImageResource차례)


1. imageview를 컨트롤해서 뿌려주는 onBindViewHolder에서 배열안에 키가 있는지 물어봐야한다.

(1) stars해쉬맵 안에 유저id키값이 있는지 없는지 물어보는 소스는 onStarClicked의 if문에 있었다. 그 부분을 복사해서, onBindViewHolder로 가져온다. 


거기서는 파라미터로 받은 키값(게시물)에 해당하는 value값들을 받을 ImageDTO를 임시로 만들어서, imageDTO.stars속에, uid있는지 확인한 뒤, 

starCount를 +1,-1하거나 uid를 포함or삭제하고, 다시 database로 setValue해버린다. 

if (imageDTO.stars.containsKey(auth.getCurrentUser().getUid())) {

여기서는 받아온 imageDTOs리스트 전체중에, i번째인 .get(position)에서 uid가 있는지 확인한 뒤, 

imageView에 그림 뿌려주기 위해서 if()까지만 가져온다(내용은x)

if ( imageDTOs.get(position).stars.containsKey(auth.getCurrentUser().getUid())) {

즉, 1번 게시물을 어댑터에서 뿌려줄 때, 1번의 stars를 불러와 내 uid가 포함되어있는지 확인하는 것임.


(2) 만약 포함되었으면 하트가 칠해진 이미지를 뿌려준다. else로 포함 안되어있으면, 테두리가 있는 이미지를 보여준다.


            if ( imageDTOs.get(position).stars.containsKey(auth.getCurrentUser().getUid())) {


                ((CustomViewHolder)holder).starButton.setImageResource(R.drawable.ic_favorite_black_24dp);

            }else{

                ((CustomViewHolder)holder).starButton.setImageResource(R.drawable.ic_favorite_border_black_24dp);

            }


2.  앱을 실행해보면, 좋아요 버튼 누를시, firebase콘솔 데이터베이스>images>각 키값> stars 안에 <유저uid, true>로 담기고, starCount가 올라간다.


3. 추가적으로 나는 starCount를 위한 textView도 추가하였다. 조심해야할 것은 데이터베이스에서 받아온 starCount는 int값으로 imageDTOs에서 저장되어있기때문에  ***텍스트뷰에 setText할 때 (""+  starCount값 ); 혹은  (String.valueOf( starCount값) ); 으로 넣어줘야한다.


            ((CustomViewHolder)holder).starCount.setText(""+imageDTOs.get(position).starCount);

or

((CustomViewHolder)holder).starCount.setText(String.valueOf(imageDTOs.get(position).starCount));



@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { //38. 갤러리에서 선택된 이미지 정보를 액티비티로 받아올 온액티비티리절트 오버라이딩
super.onActivityResult(requestCode, resultCode, data);

if(requestCode == GALLERY_CODE && data!=null){ // && data!=null 과 else문을 통해서 이미지 선택안되도 선택되도록 띄우라고 만들었다!
//System.out.println(data.getData());
//System.out.println(getPath(data.getData()));//40. 그냥 인텐트를 받은 것과, 인텐트를 이미지 경로로 바꿔줄 매쏘드를 이용한 것의 차이점을 보기 위한 테스트

//52. 얻어온 이미지 경로를 파일객체에 바로 담지말고, 51.에서 선언한 전역변수 String에 한번 담은후, string변수를 f에 넘겨, 저장되도록 하기
imagePath = getPath(data.getData());


//47. 파일객체에 갤러리선택이미지의 경로 담기 -> 52번에서 String imagePath에 저장후 담아주기로 바꿈
File f = new File( imagePath );
//50. file객체를 imageview에 뿌려주는 방법
imageView.setImageURI(Uri.fromFile(f));
}else {
Toast.makeText(getApplicationContext(), "업로드할 이미지을 먼저 선택하세요.", Toast.LENGTH_SHORT).show();
}
}

1. onActivityResult()에서 if문에 조건을 하나 더준다. 갤러리 선택했고, data가 null이 아닐 때만, imagePath에 경로를 더하도록 하고, else에서는 토스트메세지를 띄워준다.



public void upload(String uri){ //46. 4345코드는 onActionResult()에 있어서, 갤러리에서 받은 정보를, 갤러리꺼지고 액티비티에서 바로 받아서 업로드 시켰던 것을===> upload()를 만들어 옮긴다.

//45. 빨간줄 난 storageRef를 위해 복붙해온 코드
StorageReference storageRef = storage.getReference();
if(uri != null) {

//43. 공식문서에서 로컬파일 업로드부분을 복사해온다.
final Uri file = Uri.fromFile(new File(uri)); //44. 복사코드 중에 getPath(data.getData())를 넣어주는 곳-> 옮겨오면서, 파라미터의 uri경로가 들어간다.
StorageReference riversRef = storageRef.child("images/" + file.getLastPathSegment());
UploadTask uploadTask = riversRef.putFile(file);

uploadTask.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
// Handle unsuccessful uploads
}
}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
@Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {

Uri downloadUrl = taskSnapshot.getDownloadUrl(); // 업로드 된 이미지의 다운로드 주소를 의미한다. 공식문서에서 자동으로 생성되어있다.

//57.setValue에 넘겨줄 데이터구조ImageDTO의 객체를 만들어, 정보를 담아준다
ImageDTO imageDTO = new ImageDTO();
imageDTO.imageUrl = downloadUrl.toString();
imageDTO.title = title.getText().toString();
imageDTO.description = description.getText().toString();
imageDTO.uid = auth.getCurrentUser().getUid();
imageDTO.userid = auth.getCurrentUser().getEmail();

imageDTO.imageName = file.getLastPathSegment(); //storage삭제를 위해, imageDTO를 database에 올릴 때, 파일명도 같이 보낸다.

//56. 사진 업로드 성공시, 데이터베이스에도 정보를 넘겨주는 코드 //58. push.도 넣어서 싸이즈 안나게 하기
database.getReference().child("images").push().setValue(imageDTO);

//내가 만든 것
Toast.makeText(getApplicationContext(), "Storage에 사진업로드와, Datebase에 storage상 사진주소, 제목, 타이틀 이 저장되었습니다.", LENGTH_SHORT).show();

}
});

}else{

ImageDTO imageDTO = new ImageDTO();
imageDTO.title = title.getText().toString();
imageDTO.description = description.getText().toString();
imageDTO.uid = auth.getCurrentUser().getUid();
imageDTO.userid = auth.getCurrentUser().getEmail();

//56. 사진 업로드 성공시, 데이터베이스에도 정보를 넘겨주는 코드 //58. push.도 넣어서 싸이즈 안나게 하기
database.getReference().child("images").push().setValue(imageDTO);

//내가 만든 것
Toast.makeText(getApplicationContext(), "사진빼고 저장되었다.", LENGTH_SHORT).show();

}
}

2. 업로드 매쏘드에서, 넘어오는 uri가 null인경우가 아닐 때, Storage에 이미지 업로드 후 성공리스너에서 이미지 경로를 업데이트 했다.

else로 uri 가 null인경우, Storage에 업로드는 하는 과정 -> 성공/실패 리스너까지 모두 필요없다. 

그리고 기존 db에 업로드 중, 이미지 경로부분을 뺀다.

imageDTO.imageUrl = downloadUrl.toString();
                imageDTO.imageName = file.getLastPathSegment(); //storage삭제를 위해, imageDTO를 database에 올릴 때, 파일명도 같이 보낸다.


3. BoardActivity.java에서 if문을 통해, imageUrl이 null인 경우는, imageView자체를 안보이고 모양도 감추도록 VIEW.GONE을 준다

(VIEW.INVISIBLE은 이미지뷰 모양이 그대로 남아있다)

if(imageDTOs.get(position).imageUrl != null) {
Glide.with(holder.itemView.getContext()).load(imageDTOs.get(position).imageUrl).into(((CustomViewHolder) holder).imageView);
}else {
((CustomViewHolder) holder).imageView.setVisibility(View.GONE);
}


4. 다음시간에 삭제까지 넣을 거라서, 일단은 기존코드로 보류

데이터베이스를 읽어들여, 액티비티에 표시하는 내용이다. db의 내용을 바꾸면, 액티비티에서도 실시간으로 바꿔진다.

( 네비게이션 메뉴 수정 -> 띄울 액티비티 생성 -> recyclerview까지 세팅)


[1] 데이터베이스 내용을 읽어올 수 있게 프로젝트를 수정하자


1. 네비게이션 메뉴에 db내용을 띄울 메뉴를 만들자(카메라 모양의 Import -> Board)

(1) 네비게이션 drawer메뉴중에, 카메라모양의 Import를 Board(게시판)으로 바꾸기 위해서, res>menu>activity_home_drawer.xml로 간다.

(2) id를 nav_board, title을 Board로 바꾼다.

(3) HomeActivity.java로 와서, onNavigationItemSelected의 if문에서 id를 nav_camera를 nav_board로 바꿔준다.

***이제 클릭시 프래그먼트를 띄우도록 만들면 이쁘지만, 시간이 없기 때문에 인텐트를 통해 액티비티를 추가하는 방식으로 할 것이다.

(4) 이제 Board클릭시 띄울 빈 액티비티를 만든다. java패키지명 > new > activity > empty activity > BoardActivity.java

(5) 이제 홈액티비티의 onNavigationItemSelected()로 돌아와, nav_board에다가 인텐트로 BoardActivity를 띄우는 코드를 넣는다. 인텐트 변수 선언없이 바로 띄우는 간단한 코드!

        if (id == R.id.nav_board) { //59. id부터시작해서 camera-> board로 게시판내용으로 바꿔준다.


            startActivity(new Intent(this, BoardActivity.class));


        }


2. BoardActivity에 RecyclerView 달아주기

(1) 만들어준 BoardActivity.java의 onCreate()부분에 인플레이트 되어있는 R.layout.activity_board를 [Ctrl+클릭]으로 빠르게 xml파일로 가보자.**

(2) 디자인탭 팔레트에서 recyclerview를 검색해서 넣어주고, 파란색 디자인화면에서 점들을 벽에 붙혀주자.

     속성창에서 지그재그선으로 선택하여, 최대로 늘려주고, id는 recycleview로 넣어주자.

(3)다시 자바파일로 와서 , 전역변수로 recyclerview를 선언해주고, onCreate()에서 id를 참조하자.

public class BoardActivity extends AppCompatActivity {

    private RecyclerView recyclerView;


(4) recyclerview에 띄울 정보가 들어있는 클래스인 ImageDTO방식의 List를 선언하자(**우측은 ArrayList<>();)

(아마도 어뎁터가 관리할 리스트항목인듯. list는 다양한 인스턴스객체로 뽑아낼 수 있다. 선택하는 것 중에 하나가 ArrayList<>인듯하다.)

(앞으로, 리스트뷰를 만들 때는 DTO형식으로 작성한 뒤, DTO형식의 ArrayList<DTO>를 가지고 어댑터를 관리하자.


cf) java.util.List 는 인터페이스 클래스이며 java.util.Collection 인터페이스를 구현한 것입니다. List 를 사용하기 위해서는 아래 클래스들중 하나로 인스턴스화 할수 있음

List listA = new ArrayList();
List listB = new LinkedList();
List listC = new Vector();
List listD = new Stack();

출처: http://mainia.tistory.com/2323

(5) 마찬가지로 String형 리스트도 하나 만들어준다(이번 강의에서 사용하진 않음)

public class BoardActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    private List<ImageDTO> imageDTOs = new ArrayList<>();

    private List<String> uidLiss = new ArrayList<>();


(6) 어댑터도 이너클래스로 만들어준다. BoardRecyclerViewAdapter를 만드는데, RecyclerView.Adapter를 extends하고, 

꺽쇠로<Viewholder로 <RecyclerView.ViewHolder>를 자동완성까지 시켜줘야한다.(커스텀 뷰홀더는 이 어댑터의 이너클래스로 만들 것이다)

    class BoardRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{


    }

(7) implements가 필요한 오류를 확인하고 3개 다 임플리먼트 시켜준다.


3. 각 item화면을 위한 xml을 추가해준다.

(1) layout폴더> 우클릭> layout resource file > item_board.xml

(2) imageview를 하나 집어넣고 컬러로 기본배경을 선택한다. 높이는 200정도로 주고 위에 붙혀준다. id는 item_imageview로 준다.

(3) 그 밑에 텍스트뷰를 2개 집어넣고 id에는 item_을 붙혀준다. 상하 순서에 맞게 차례대로 점을 이어준다. 그렇지 않으면 (4)시 꼬이게 된다.

**(4)각 item의 최상위 레이아웃의 높이은 wrap_content로 바꿔줘야한다.


4. 이제 item화면인 item_board.xml를 어댑터에 붙혀줘야한다.

(1)어댑터 속  onCreateViewHolder()에서 View 변수를 하나 만들어서 인플레이트 시켜준다. 이때, 뷰홀더의 부모뷰그룹.을 이용해서 getContext()한다.

            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_board, parent, false);


(2) return을 할때는, CustomViewHolder 객체를 생성함과 동시에 파라미터로 view를 넘겨준다.  이때 [ alt + enter ]를 통해 inner class를 어댑터내부에 만들어준다.

            return new CustomViewHolder(view);


(3) 이제 커스텀뷰홀더 클래스를 수정하는데, 먼저 생성자 속 super();에다가 view를 넣어준다.

참고) super에 대한 설명 : http://neoroid.tistory.com/10

**** super(); 는 부모클래스(리사이클러뷰의.뷰홀더)의 생성자를 호출해라.

**** super. xxx(); 부모클래스속의 매쏘드/변수를 호출해라


        private class CustomViewHolder extends RecyclerView.ViewHolder {

            public CustomViewHolder(View view) {

                super(view);

            }


(4) 다음으로 이너클래스 전역변수로, item_board안에 있는 view인 imageview와 textview를 선언해준다. 이때는 private가 필요없다. 


(5)이제 CustomViewHolder의 생성자 안에서, view.을 붙혀서, id를 찾아줄 것이다.

            public CustomViewHolder(View view) {

                super(view);

                

                imageView = (ImageView) view.findViewById(R.id.item_imageView);

                textView = (TextView) view.findViewById(R.id.item_textView);

                textView2 = (TextView) view.findViewById(R.id.item_textView2);

                

            }


정리)

지금까지 어댑터 세팅이었다. onCreateViewHolder에서는 View변수를 생성해서 인플레이트시켜해놓고, return을 view를 바로하는 것이 아니라

CustomViewHolder를 만들어, 객체의 파라미터로 view를 넣어서 return 해야한다.

CustomViewHolder는, 인플레이션 된 item_board.xml에 속에 있는 뷰들을 선언한 뒤/ 생성자에서 view.객체를 이용해서 id를 참조시킨다.


5. 이제 마지막 recyclerview에 어댑터를 세팅해주자.

(1) recyclerview의 id를 참조한 곳 밑에다가 recyclerview.setLayoutManager()을 통해 LayoutManager를 설정해준다. 

그렇지 않으면 default된 상태가 되서 사용할 수 없다. recyclerview라면 무조건 지정해줘야한다.

(2) 정의한 어댑터변수를 만들고 객체를 생성해준 뒤, recyclerview.에 setAdapter해준다.

        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        BoardRecyclerViewAdapter boardRecyclerViewAdapter = new BoardRecyclerViewAdapter();

        recyclerView.setAdapter(boardRecyclerViewAdapter);




[2] firebase 콘솔에서 데이터베이스를 가져와보자.

1. BoardActivity의 전역변수로 Firebasedatabase를 선언하고, 다른 클래스에도 쓰기위해 onCreate()에서 싱글톤 패턴으로 참조한다.


    private FirebaseDatabase database;

...

database = FirebaseDatabase.getInstance();


2. firebase콘솔에서 데이터베이스를 확인해주자. 폴더명인 images도 확인해주자.


3. onCreate()에서 recycler에 어댑터 달아준 곳 밑에다가, database변수를 활용해서 .getReference().를 통해 데이터베이스에 접근하고, child()에 우리가 만들었던 폴더명인 "images"를 넣어서 이미지폴더에 접근한 뒤 , addValueEventListener를 설정해주자. 파라미터로 (new Value 까지 쳐서 ValueEventListener()를 달아주자.

 - addValueEventListener는 문서확인해보면, observer패턴으로서,  글자가 하나씩 바뀔때마다 리스너의 onDataChange가 호출되면서 dataSnapshot이라는 파라미터로 넘어온다. 우리는 이것이 호출될 때, 전체 데이터를 다시 가져와, imageDTOs라는 list에 담아, 리사이클러뷰에 리스트를 보여줄 것이다.


        database.getReference().child("images").addValueEventListener(new ValueEventListener() {

            @Override

            public void onDataChange(DataSnapshot dataSnapshot) {


                

            }


            @Override

            public void onCancelled(DatabaseError databaseError) {


            }

        });

    }


(1) onDataChange()에서 날라오는 것을 list<imageDTO>인 imageDTOs에 담을 것인데, 먼저 리스트를 .clear(); 해주어야한다. 그렇지 않으면 날라올때마다 계속 쌓이게 된다.

                imageDTOs.clear();

(2) for문을 이용해 날라온 dataSnapshot안의 내용물들을 처음부터~끝까지 꺼낼 수 있는 문장을 만든다. 그 하나하나는 snapshot이라는 변수에 들어간다.

(3) firebase에 ImageDTO형태로 넣었었으니, 꺼낸 것을 받을 때도, imageDTO 변수를 하나 만들어서 받아야한다. 

snapshot안의 imageDTO형태들을 getValue()을 이용해서 꺼낼 수 있다. 파라미터로는, Obecjt class인 ImageDTO.class가 들어간다.

(4) 해당 반복문 상태에서 꺼내온 imageDTO를  imageDTO 리스트인 imageDTOs에  .add()로 다 담는다.

(5) 전체 데이터를 받고 난 뒤, recycler뷰의 어댑터가 갱신이 되어야한다. 

항상 먼가를 받고나서 바로 다음줄에 어댑터객체.를 이용해서 notifyDatasetChanged();를 넣어주자.

이때, 어댑터를 final로 바꿔서 리스너안에서도 쓸수있게 해도 되지만 전역변수로 줘서 사용하자.


            public void onDataChange(DataSnapshot dataSnapshot) {


                imageDTOs.clear();

                for(DataSnapshot snapshot : dataSnapshot.getChildren()){


                    ImageDTO imageDTO = snapshot.getValue(ImageDTO.class);


                    imageDTOs.add(imageDTO);


                }

                boardRecyclerViewAdapter.notifyDataSetChanged();


            }



정리) firebase콘솔-database-images에 있는 imageDTO형식의 자료들

-> addValueEventListener() 속 onDataChange() -> 

dataSnopshot -> 리스트.clear() -> for문/ getChildren() -> snapshot -> getValue(ImageDTO.class)-> imageDTO-> imageDTOs.add(); /for문끝->

 어댑터.notifyDataSetChanged();


[요약] 

onCreateViewHolder()에서는 item.xml을 인플레이트 한 것을 view변수에 담고, 그 view변수를 파라미터로 한 커스텀뷰홀더를 리턴한다.

onBindViewHolder()에서는 (커스텀뷰홀더에서 선언하고 참조한) 캐스팅된 홀더객체를 이용해 텍스트뷰, 이미지뷰 등의 뷰들을 조작해서 뿌려준다.
private class CustomViewHolder()에서는, 전역변수로 뷰들을 선언하고, 생성자내에서 id를 참조한다.


4. 이제 바뀐 정보를 전체로 받은 imageDTOs를 아이템의 position에 따라 사용되도록, 어댑터에서 세팅해준다.

(1) BoardRecyclerViewAdapter의 getItemCount()에는 imageDTOs.size();를 리턴 

(2) onBindViewHolder()에서는 CustomViewHolder()에서 참조된 imageview와 textview를 사용할 수 있는 곳이다. 

(position이 파라미터로 있는 곳, 리스트에서 꺼내 쓸수있다)

파라미터인 holder를 사용하긴 해야하는데, customviewholder를 썼으니, ((CustomViewHolder)holder).라고 캐스팅 해서 사용해 준다.

 (희안하게, holder.이 아니라 holder).이다. 캐스팅괄호1, holder괄호2 총 2개 필요함)

 - imageDTOs.get(position).  title 등을 꺼낼 때, 잘 모르겠으면, firebase의 콘솔을 보고 변수명을 파악한다.


        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {


            ((CustomViewHolder)holder).textView.setText(imageDTOs.get(position).title);

            ((CustomViewHolder)holder).textView2.setText(imageDTOs.get(position).description);


        }



5. 이제 인터넷 uri를 가지고 imageview에 뿌려줄 수 있는 라이브러리를 추가해줘야한다.

(1) app> 우클릭 > open module settings(ctrl+alt+shift+s) > dependecies > + > library dependency 에서 'glide' 검색하기(나는 검색안됨)

(2) 나는 검색이 안되서 compile해주기로 함. 깃헙 glide 공식홈페이지에서,  repositories 는 안건들고, 모듈gradle에서

    implementation 'com.github.bumptech.glide:glide:4.6.1'

    annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'

만 추가해줬니 된다.


***(3) 이제 BoardActivity의 onBindViewHolder에다가 Glide.with()를 쓰는데, 캐스팅없이 파라미터 holder.를 이용해 itemView.getContext()를 통해 context객체를 바로 받아올 수 있다.

into()에다가는 imageview를 가진 캐스팅한 holder).를 이용해서 imageview를 찾아준다.


            Glide.with(holder.itemView.getContext()).load(imageDTOs.get(position).imageUrl).into(((CustomViewHolder)holder).imageView);


6. 이제 앱을 켠 뒤, Board를 확인하고, firebase콘솔에서 database를 변경한 다음 앱화면에서도 바뀌는 것을 확인해보자.












질문과 답변


Q. 회원정보(?)같은 액티비티를 만들고 있는데, 여러 데이터들을 구조체를 이용해 한번에 파이어 베이스에 올리는 것은 성공 했습니다. 저는 예제와는 다르게 이 데이터를 다시 같은 액티비티에 가져와 표시한 후 수정할 수 있도록 만들려고 하고 있는데요, 

예제에 나온 방식대로 database.getReference().child("images").addValueEventListener를 이용한 후 for문으로 데이터를 가져오니 앱이 죽어버립니다. 디버깅을 해보니 정상적으로 데이터를 다시 구조체에 받아오기는 합니다. (Recycle 뷰나, list뷰는 쓰지 않아서 구조체 리스트는 만들지 않았습니다.)  

조금 더 간단하게 테스트를 해보기 위해 EditText 두개와 Button 하나로 비슷한 테스트를 해 보았지만, 역시나 데이터를 올리고 받아오기는 하지만, EditText에 받아온 데이터가 setText로 들어가지지 않고 그냥 앱이 종료가 됩니다. 반쯤 포기하고 그냥 DB에 업로드만 하고  SharedPreference로 앱 내부에 값을 저장해서 불러와야 하나 고민중에 있습니다. 어디가 잘못 된 것일까요ㅠㅠ  


database.getReference().child("informs").addValueEventListener(new ValueEventListener() {

            @Override

            public void onDataChange(DataSnapshot dataSnapshot) {

                DTO dto = new DTO();

                for (DataSnapshot snapshot : dataSnapshot.getChildren()){

                    dto = snapshot.getValue(DTO.class);

                }               

            }


            @Override

            public void onCancelled(DatabaseError databaseError) {


            }

        });


값을 받아오는 코드는 이런식으로 작성했고, 위치는 onCreate 안에 있습니다. 혹시나 싶어 onStart 안에 넣어보았지만, 데이터를 구조체에 넣기만 하면 그냥 앱이 죽어버립니다...


A. 흠... 혹시 DTO.class 모델이랑 images 데이터베이스 구조를 볼 수 있나요? 일단 구조가  DTO.class와 데이터베이스의 images와 매핑이 되지 않는 것 같습니다.




Q. 답변 감사합니다!! 혹시 마지막으로 listview가 이벤트뒤에 출력되고 db에도 저장되어있는데 앱을 껏다켜도 화면에 그대로 남아있게 하는방법이있을까요? 저는 출력되지만 앱을껏다키면 화면에서 사라져서요


A. 흠 여러가지 방법이 있지만 일단 SharedPreferences에 값을 저장하는 방법이 있습니다.  문제는 SharedPreferences 리스트가 저장되지 않기때문에 값을 리스트 값을 - JSON으로 바꿔서 스트링을 SharedPreferences 에 저장한 후 읽어 드릴때 JSON -> LIST로 만들어서 출력하면 될 것 같습니다.




Q. 질문하나만 더 드려도될까요? 제가 로그인정보를 usermodel에 저장하고 유저의 이름을 화면에 출력하고싶어서 하울님의 코딩그대로 snapshot을 포문을돌려서 리스트에저장해서 출력을했는데 이렇게하면 child("users")에 있는 모든 값을 다 가져오더라구요. 그래서 로그인한 유저의 값만 snapshot 저것을 사용해서 1개만 가져오려면 어떻게해야할까요?


A. 그럴 경우 한번 더 경로를 타고 들어가야 됩니다.  Database.getIntance().getReference().child("users").child(로그인한 UID).add..를 입력하시면 될 것 같습니다.




Q. 안녕하세요 강의 잘보고있습니다! 저는 커스텀다이얼로그에 값을입력하고 postive버튼을누르면 firebase db에 저장되고 화면에 listview로 띄우는 식으로 구현을 하였는데요, 앱을 껏다가 다시키면 정보들이 db에는 저장되어있지만 화면에는 남아있지않습니다. 해결방법이 있을까요?


A. 흠.. 디비에 저장됬는데 화면에 안나온다는 건가요..? 그건 데이터 스키마가 안맞을 경우 데이터를 못 불러오는 경우도 있습니다. 한번 스키마 구조가 맞는지 확인하시면 될것 같습니다.


아니요! 화면에 나옵니다 잘나오는데 앱을 껏다가 다시 실행하면 원래 나오던 리스트들이 화면에나오지않습니다. db에는 저장되있지많요. 다시 추가해주면 화면에 보이구요. 물론 그것또한 db에 저장되구요. 그니까 예를들어 버튼을눌러서 db에 저장되고 화면이 출력이되는데 앱을껏다키면 db에는 남아있지만 그것들을 다시화면에불러오지는못합니다. 다시버튼을누를시 똑같이화면에추가되고 db에도 추가되구요



Q.

안녕하세요 강의 잘보고 있습니다. 저는 강사님처럼 똑같이 코딩한뒤에 Board 부분 부르면 com.google.firebase.database.DatabaseException: Can't convert object of type java.lang.String to type 이런 에러 뜨는데 

A. 

이 에러는 Json을 Object화 하면서 나오는 에러인것 같습니다. 일단 ImageDTO 클래스의 대소문자라든가 혹은 int와 String이 제대로 선언됬는지 보면 좋을 것 같습니다. 혹은 48번째줄 ImageDTO imageDTO = snapshot.getValue(ImageDTO.class)라고 제대로 입력됬는지 확인하시면 될 것 같습니다.

===> 나의 경우 지웠다가 ImageDTO를 작성하면서 제대로 선택해주니까.. 되었다.. 




Q. 

json을 csv파일로 바꿔서 해야하는데 csv파일을 엑셀에 어떻게 적용시켜야할까요

A. FireBase에서 function이용 하시면 될것 같구요 거기서 jsontoexcel 모듈을 이용하셔야 할것 같습니다 참고로 functions은 자바스트립트 기반입니다

Q. 

파이어베이스와 엑셀 연동이 가능할까요?  파이어베이스에 저장된 데이터를 엑셀파일에 적용시키게 하고 싶어서요

A.

일단 메일로 답변 드렸는데 한번시도해 보시고 다시 질문 주시면 좋을것 같습니다~



+ Recent posts