ADMOB Error 3 : ad failed to load 3

애드몹을 쓰던 중, 잘만 나오던 광고가 아래 오류가 어느 순간 갑자기 나오기 시작하면서 테스트 배너 광고가 나오지 않았습니다. 그래서 어떤 오류인가 했더니 아래와 같은 오류였네요.

광고 요청은 성공(그러니까 구현에는 문제가 없다는것)했는데, 인벤토리에 광고가 없어서 광고를 못보여준다고합니다. 어, 하지만 스토어에 있는 내 앱에는 광고가 잘 나오는데요? 저의 증상은 출시 앱에는 광고가 잘 나오고, 오히려 테스트 ID를 써서 앱 개발할때에는 테스트 광고가 나오지 않는 것이었습니다.

구글링을 해보니 광고가 잘 나오다가 이와 같은 증상을 겪은 사람들의 공통점이 어느 순간 갑자기 안나온다는 것이었습니다. 몇가지 조치할 수 있는 방법이 있는데 한번 확인해보세요.

 

1. 구글 플레이 스토어에 구글 광고가 포함되어있다고 체크했는지 확인

구글 플레이 콘솔 -> 출시 앱 선택 -> 왼쪽 하단 앱 콘텐츠 선택 -> 중간쯤에 광고란 확인

 

2. 부정클릭이 발생했는지 확인. 이 경우 30일간 광고가 일시 정지되거나 영구정지 될 수도 있다고 하네요. 어떤 경우라고 자신의 광고를 클릭하지 맙시다.

 

3. app-ads.txt를 추가하고 테스트 기기 등록했는지 확인

저의 경우는 3번이었습니다. 갑자기 안나온것이라 생각했는데, 기억을 더듬어 보니 app-ads.txt를 추가한 이후에 테스트 광고가 안나오는 것이었어요. 역시 나만 갑자기라고 생각했나봐요..

이것에 대한 해결 방법에는 두가지가 있습니다.

1. app-ads.txt에 아래의 구문 추가

google.com, pub-3940256099942544, DIRECT, f08c47fec0942fa0

이 방법은 간단하긴 하지만 반영되기까지 최대 24시간이 있어야한다는 군요.

 

2. Test Device 등록 

이 방법은 바로 적용이 가능한 방법입니다. 저는 바로 확인을 원하기 때문에 2번, 테스트 기기를 등록하는 방법을 사용할 것입니다.

그래서 이번 포스팅은 안드로이드 테스트 디바이스를 설정하는 방법에 대한 것입니다. 이때 테스트 광고 ID를 쓰는 것이 아니고 발급받은 ID를 사용해야한다는 점입니다.

아래의 코드를 추가하세요.

	MobileAds.initialize(this);
        //앱 출시시 반드시 주석 처리
        List<String> testDeviceIds = Arrays.asList("Your Device ID");
        RequestConfiguration configuration =
                new RequestConfiguration.Builder().setTestDeviceIds(testDeviceIds).build();
        MobileAds.setRequestConfiguration(configuration);
        //앱 출시시 반드시 주석 처리

Arrays.asList에는 자신의 고유 Device ID를 기재해주어야합니다. 어떻게 아냐구요?  안드로이드 스튜디오 Logcat에 나와있습니다.

I/Ads: Use RequestConfiguration.Builder.setTestDeviceIds(Arrays.asList("33BE2250B43518CCDA7DE426D04EE231"))
to get test ads on this device."

Logcat에서 RequestConfiguration을 검색하여 찾아보세요.

 

다음으로 중요한 것은 Test 광고 ID를 사용하는 것이라고 했죠? adUnitId를 발급받은 광고 ID로 바꿔주세요. 

banner_ad_unit_id

        <com.google.android.gms.ads.AdView
            xmlns:ads="http://schemas.android.com/apk/res-auto"
            android:id="@+id/ad_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_alignParentBottom="true"
            ads:adSize="SMART_BANNER"
            android:layout_alignParentEnd="true"
            ads:adUnitId="@string/banner_ad_unit_id" />

 

이렇게 하면 테스트 광고가 잘 나올겁니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

Module Import 버그 해결 방법

Android Studio Arctic Fox의 버그인지는 모르겠지만 Native Template의 모듈을 가져오는게 안됩니다. Native Template를 받으려면 아래의 주소에서 다운받으시면 됩니다.

https://github.com/googleads/googleads-mobile-android-native-templates

 

GitHub - googleads/googleads-mobile-android-native-templates

Contribute to googleads/googleads-mobile-android-native-templates development by creating an account on GitHub.

github.com

 

압축풀면 아래와 같이 nativetemplates라는 폴더하나가 생기게 됩니다. 이 폴더를 Import하는 것이 목표입니다.

 

원래는 아래와 같이 Import하시면 됩니다.

일반적인 경우

1. Android Studio를 켜서 File -> New -> Import Module를 누릅니다.

 

2. 아까 다운 받은 그 폴더를 설정해주고 Finish를 누르면 됩니다. 아래 사진에서는 Module Name이 :nativetemplate이지만 :nativetemplates로 해주셔야해요. 저는 어떤 이유인지는 모르겠지만 Finish가 활성화되어있지 않고 이 방법으로 모듈을 Import할 수 없었습니다.

 

다른 방법으로 Import

1. 아래와 같이 다운받은 폴더를 같은 프로젝트에 둡니다. 

 

2. settigs.gradle 파일에 nativetemplates를 추가시켜줍니다. 

프로젝트 수준의 settigns.gradle

include ':app',':nativetemplates'

 

3. 한가지 추가적으로 nativetemplates의 build.gradle의 버전을 맞춰줘야합니다. 자신의 기존 프로젝트와 같이 sdk버전을 맞춰주시면 됩니다. 그리고 sync를 눌러주세요.

nativetemplates의 build.gradle

기존 프로젝트 sdk 버전과 일치시켜야함

 

여기까지 완료했으면 아래와 같이 implementation해도 오류나지 않을 겁니다. 

implementation project(':nativetemplates')

 

아래와 같이 TemplateView를 가져올 수 있는것 확인할 수가 있네요.

 

도움이 되셨다면 좋아요부탁드립니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

SwipeRefreshLayout

요즘 어플리케이션들을 보면 아래로 당겨서 화면을 새로 로딩하는 기능이 많죠. 이런 기능을 담당하는 것이 SwipeRefreshLayout입니다. 그래서 아래로 당기면 아래처럼 로딩하는 화면(빙글 빙글 돌아가는 로딩화면)이 나온 이후에 로딩이 완료되면 어플리케이션의 정보를 갱신해줍니다.

 

 

SwipreRefreshLayout을 사용하기 위해서는 build.gradle 파일에 아래처럼 종속 항목을 명시해줍니다. 

build.gradle

 

build.gradle

dependencies {
	//...
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
    //...
}

 

현재 안정화버전은 1.1.0버전이기 때문에 1.1.0버전을 사용하도록 하겠습니다. 추후에 안정화버전이 변경될 수 있으니 안드로이드 개발 문서를 참고해주시기 바랍니다.

https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout?hl=ko#1.1.0 

 

Swiperefreshlayout  |  Android 개발자  |  Android Developers

Swiperefreshlayout 스와이프하여 새로고침 UI 패턴을 구현합니다. 최근 업데이트 현재 안정화 버전 다음 버전 후보 베타 버전 알파 버전 2020년 7월 22일 1.1.0 - - 1.2.0-alpha01 종속 항목 선언 SwipeRefreshLayout

developer.android.com

 

이제 sync를 하고 나면 벌써 사용할 준비는 끝이 났습니다. 이제 메인화면을 구성할 activity_main.xml을 만들어줍시다.

activity_main.xml

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id='@+id/refresh_layout'>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:id="@+id/text_view"
            android:text="아래로 당겨보세요."/>
    </RelativeLayout>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

 

SwipeRefreshLayout에 새로고침이 될 레이아웃을 추가하였습니다. 저는 간단히 RelativeLayout안의 TextView의 텍스트를 새로운 데이터로 고쳐보도록 하겠습니다. 이것을 프로그램한것이 MainActivity에 존재합니다.

MainActivity.java 

import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private SwipeRefreshLayout swipeRefreshLayout;
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        swipeRefreshLayout=(SwipeRefreshLayout)findViewById(R.id.refresh_layout);
        textView=(TextView)findViewById(R.id.text_view);

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                textView.setText("새로 고침 완료");
                swipeRefreshLayout.setRefreshing(false);
            }
        });
    }
}

 

누가봐도 매우 간단한 코드네요. SwipeRefreshLayout에 OnRefreshListener를 추가해주면 새로고침이 시작되었을때 onRefresh 메소드가 호출이 됩니다. 이때 다시 로드하는 코드를 추가하여 주고 setRefreshing에 false를 전달하여 로딩중인 화면을 없애주면 됩니다.

 

실행하면 화면이 로딩된 이후 TextView에서 로딩이 완료되었다는 메시지와 함께 Refreshing을 중지합니다.

 

만약 쓰레드를 이용하면 onRefresh에 쓰레드를 실행하여 Handler를 통해 완료 메시지를 받으면 여기서 setRefreshing을 false로 전달해주면 됩니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

특정 시간에 Notification 발생 시키기

 

특정 시간이 되면 알림이 발생되는 코드를 android에서 구현해보도록 하겠습니다. 특정 시간을 설정해서 그 시간이 되면 알려주는 무엇인가가 있어야하는데 이를 가능하게 만들어주는 것이 AlarmManager입니다. 코드를 통해서 알아보도록 합시다.

 

먼저 레이아웃은 간단하게 아래처럼 구성되어있습니다.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TimePicker
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:timePickerMode="spinner"
        android:id="@+id/time_picker" />


    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="저장"
        android:id="@+id/save"/>

</LinearLayout>

 

이제 time_picker에서 시간을 설정하고 save 버튼을 누르게 되면 그 시간에 Notification이 발생하게 되는 코드를 설명합니다.

 

MainActivity

MainActivity의 onCreate 코드는 아래와 같습니다. 

package com.reak.alarmtest;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TimePicker;
import android.widget.Toast;

import java.util.Calendar;

public class MainActivity extends AppCompatActivity {

    private Button save;
    private TimePicker timePicker;

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        timePicker=(TimePicker)findViewById(R.id.time_picker);
        save=(Button)findViewById(R.id.save);

        save.setOnClickListener(v->{

            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(System.currentTimeMillis());
            int hour=timePicker.getHour();
            int minute=timePicker.getMinute();
            calendar.set(Calendar.HOUR_OF_DAY,hour);
            calendar.set(Calendar.MINUTE,minute);

            if (calendar.before(Calendar.getInstance())) {
                calendar.add(Calendar.DATE, 1);
            }

            AlarmManager alarmManager=(AlarmManager)this.getSystemService(Context.ALARM_SERVICE);
            if (alarmManager != null) {
                Intent intent = new Intent(this, AlarmReceiver.class);
                PendingIntent alarmIntent = PendingIntent.getBroadcast(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);

                alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                        AlarmManager.INTERVAL_DAY, alarmIntent);

                Toast.makeText(MainActivity.this,"알람이 저장되었습니다.",Toast.LENGTH_LONG).show();
            }
        });
    }
}

 

여기서 Calendar 객체를 현재시간으로 미리 설정해두고, set 메소드로 timePicker에서 설정된 시간과 분으로 설정시키는 것입니다. 이때 시간과 분을 TimePicker에서 얻어오려면 @RequiresApi(api = Build.VERSION_CODES.M) 를 메소드위에 추가시켜야합니다.

이제 AlarmManager를 가져옵니다. Intent는 수신자 클래스를 전달하게 됩니다. 여기서는 AlarmReceiver라는 수신자이며 밑에서 이 Receiver를 구현하게 될겁니다. 그리고 PendingIntent를 얻어와서 setRepeating 메소드로 정확한 시간에 알람을 설정시켜줍니다.

 

여기서 setRepeating메소드는 알람을 반복시키는 메소드입니다. 여기서는 AlarmManager.INTERVAL_DAY를 사용하였고, 이것은 매일 알람이 울릴것을 명시해준것입니다. 

 

굳이 정확한 시간에 알람을 울리지 않을 경우에는 setInexactRepeating 메소드를 사용할 수도 있습니다. 실제로 안드로이드 개발문서에는 이 메소드를 사용하라고 권고하고 있네요.

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.reak.alarmtest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AlarmTest">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver android:name=".AlarmReceiver"/>

    </application>

</manifest>

 

수신자를 사용하려면 위에서처럼 메니페스트에 수신자를 명시해주어야합니다. 

 

AlarmReceiver

package com.reak.alarmtest;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.RingtoneManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import androidx.core.app.NotificationCompat;

import java.util.Calendar;
import java.util.StringTokenizer;

import static android.app.Notification.EXTRA_NOTIFICATION_ID;

public class AlarmReceiver extends BroadcastReceiver {

    private Context context;
    private String channelId="alarm_channel";
    @Override
    public void onReceive(Context context, Intent intent) {
        this.context = context;


        Intent busRouteIntent = new Intent(context, MainActivity.class);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addNextIntentWithParentStack(busRouteIntent);
        PendingIntent busRoutePendingIntent =
                stackBuilder.getPendingIntent(1, PendingIntent.FLAG_UPDATE_CURRENT);

        final NotificationCompat.Builder notificationBuilder=new NotificationCompat.Builder(context,channelId)
                .setSmallIcon(R.mipmap.ic_launcher).setDefaults(Notification.DEFAULT_ALL)
                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
                .setAutoCancel(true)
                .setContentTitle("알람")
                .setContentText("울림")
                .setContentIntent(busRoutePendingIntent);


        final NotificationManager notificationManager=(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            NotificationChannel channel=new NotificationChannel(channelId,"Channel human readable title",NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }

        int id=(int)System.currentTimeMillis();

        notificationManager.notify(id,notificationBuilder.build());

    }
}

 

이제 알람이 발생되면 AlarmReceiver의 onReceive 메소드가 호출되게 됩니다. 이때 아까 우리가 MainActivity에서 넘긴 PendingIntent를 requestCode를 통해서 얻어올 수가 있습니다. 이 requestCode는 알람을 설정했던 것과 같은 코드를 사용해야합니다. 여기서는 1입니다.

 

오레오(Oreo)버전 이상부터는 NotificationChannel을 명시해주어야합니다. 그 코드도 적용되어 있습니다.

 

대충 완성하게 되면 아래와 같은 모습입니다. 앱 디자인은 개나 줘버린 모습이지만 구현이 제대로 되었나 확인만 하는 용도입니다.

alarm test 앱화면

 

그리고 원하는 시간에 알람을 저장시켜줍니다. 그래서 9시 15분에 알람을 설정합니다. 그리고 그 시간에 울리는 지 잠깐 기다려주겠습니다. 이때 9시 14분이었고, 1분만 기다리면 됩니다.

alarm 저장

 

다음은 알람이 발생되어 Notification이 발생된 화면입니다.

 

분명 9시 15분이었는데, 9시 16분에 알람이 발생되었군요. 정확한 시간에 알람이 딱 울리지는 않는 것 같습니다. 제 생각에는 setRepeating이 setInexactRepeating메소드보다 더 정확한 시간에 발생시켜주는 것으로 확인이 됩니다.

알람 취소

그리고 알람을 취소하려면 아래와 같이 코드를 추가하면 되는데요.  위의 코드는 없지만 만약 알람 취소버튼을 추가하고 그 버튼이 눌리면 alarmManager의 cancel 메소드를 이용하면 됩니다.

PendingIntent cancelIntent=PendingIntent.getBroadcast(MainActivity.this, 1, intent,PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(cancelIntent);

 

 

여기까지 Alarm과 Notification, Receiver를 조합하여 앱을 구현해보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

XmlPullParser

Xml을 안드로이드에서 파싱하는 방법은 XmlPullParser라는 녀석으로 파싱할 수 있습니다. 사용법만 알면 누구든 쉽게 파싱할 수 있습니다.

일단 다음과 같은 xml이 있다고 쳐보도록 하겠습니다. 아래는 공공데이터포탈의 버스 위치 정보의 xml파일입니다. 

...
<busLocationList>
  <endBus>0</endBus>
  <lowPlate>0</lowPlate>
  <plateNo>경기70사1109</plateNo>
  <plateType>3</plateType>
  <remainSeatCnt>38</remainSeatCnt>
  <routeId>233000031</routeId>
  <stationId>202000217</stationId>
  <stationSeq>60</stationSeq>
</busLocationList>
<busLocationList>
  <endBus>0</endBus>
  <lowPlate>0</lowPlate>
  <plateNo>경기70사1139</plateNo>
  <plateType>3</plateType>
  <remainSeatCnt>42</remainSeatCnt>
  <routeId>233000031</routeId>
  <stationId>200000321</stationId>
  <stationSeq>8</stationSeq>
</busLocationList>
...

 

위의 xml을 파싱하는 방법은 아래의 코드 예제로 이해가 가능합니다. 우선 두가지 클래스 XmlPullParser와 XmlPullParserFactory가 필요합니다. 그리고 예외를 핸들링하려면 XmlPullParserException가 필요합니다. 이것들을 우선 import하고 아래의 코드를 통해 봅시다. 

import org.xmlpull.v1.XmlPullParser;

import org.xmlpull.v1.XmlPullParserException;

import org.xmlpull.v1.XmlPullParserFactory;

 

우선 웹에서 응답을 가져오는 코드이기 때문에 Thread를 사용하여야합니다. 아래의 코드는 run을 오버라이드한 코드입니다.

 

    @Override
    public void run() {

        try {

            URL url = new URL(requestUrl);
           
            XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
            parser = xmlPullParserFactory.newPullParser();
         
            InputStream is = url.openStream();
            parser.setInput(new InputStreamReader(is, "UTF-8"));
            String tagName="";
            
            //event type얻어오기
			int eventType = parser.getEventType();

			//xml문서의 끝까지 읽기
			while (eventType != XmlPullParser.END_DOCUMENT) {
				switch (eventType) {
				//태그가 시작
					case XmlPullParser.START_TAG:
						tagName=parser.getName();
						if (parser.getName().equals("busLocationList")) {
							//객체 생성
							BusLocation location=new BusLocation();
						}
						break;
				//태그의 끝
					case XmlPullParser.END_TAG:
						if (parser.getName().equals("busLocationList")) {
							//객체를 리스트에 추가
							locationList.add(location);
						}
						break;
				//태그 안의 텍스트
					case XmlPullParser.TEXT:
				
						switch(tagName) {
							case "endBus":{
								location.endBus=parser.getText();
								break;
							}
							case "lowPlate":{
								location.lowPlate=parser.getText();
								break;
							}
							case "plateNo":{
								location.plateNo=parser.getText();
								break;
							}
							case "plateType":{
								location.plateType=parser.getText();
								break;
							}
							case "remainSeatCnt":{
								location.remainSeatCnt=parser.getText();
								break;
							}
							case "routeId":{
								location.routeId=parser.getText();
								break;
							}
							case "stationId":{
								location.stationId=parser.getText();
								break;
							}
							case "stationSeq":{
								location.stationSeq=parser.getText();
								break;
							}
						}
						break;
				}
                //다음으로 이동
				eventType = parser.next();
			}

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

 

while을 통해서 xml의 끝까지 돌아야하는데, 이때 XmlPullParser의 END_DOCUMENT를 통해서 알 수 있습니다. 그리고 그 외 시작 태그를 알 수 있는데 XmlPullParser의 START_TAG로 알 수 있습니다. 비슷하게 태그가 닫히는 부분은 END_TAG로 알 수 있습니다.  태그의 이름은 getName으로 알 수 있지요.

태그안의 내용은 XmlPullParser의 TEXT로 알 수 있고, 실제 내용은 getText 메소드로 알 수 있습니다. 위 코드는 응답받은 xml을 busLocationList 항목의 리스트로 저장하는 코드입니다.

사용하는 방법은 그리 어렵지 않죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

서울시 버스 도착 정보 조회

 

● API 사용 신청하기

 

1. 회원가입 후 로그인 진행

우선 아래의 공공데이터 포털에 접속 한 후 회원가입후 로그인을 합니다.

www.data.go.kr/

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

 

2. 원하는 Open API를 검색

로그인을 하고 난 이후에 맨 위 상단에 데이터찾기 -> 데이터목록 을 찾아 클릭 한 후 검색 조건을 지정하여 API를 검색할 것입니다. 이때 우리가 사용할 API는 서울시 버스 도착 정보 조회입니다.

그냥 검색하면 너무 많은 버스 관련 API들이 검색되기 때문에 제공기관별검색을 눌러 자치행정기관 - 서울특별시 - 서울특별시를 선택해줍시다.

 

 

그 후 검색창에 "버스 조회" 를 검색하면 중간 부분에 오픈 API가 검색이 되는데 버스도착정보조회 서비스가 우리가 사용할 API가 됩니다.

 

눌러서 들어가보면 4개의 API를 사용할 수가 있는 것을 볼 수 있습니다.

 

API에 대한 간단한 설명을 아래의 표로 정리하였습니다.

일련번호 

API명(국문) 

상세기능명(영문) 

상세기능명(국문) 

1 

버스도착정보조회 서비스 

getArrInfoByRouteAllList 

경유노선전체정류소별도착예정정보목록조회 

2 

버스도착정보조회 서비스 

getArrInfoByRouteList 

정류소노선별도착예정정보목록조회 

3 

버스도착정보조회 서비스 

getLowArrInfoByRouteList 

정류소의특정노선교통약자용도착예정정보목록조회 

4 

버스도착정보조회 서비스 

getLowArrInfoByStIdList 

정류소별교통약자용도착예정정보목록조회 

 

3. 활용신청

검색했다고 그냥 쓸 수 있는 것은 아니고 활용 신청하면 API를 사용할 수 있습니다. 오른쪽 활용신청을 클릭해줍니다.

 

여기서 활용목적은 앱개발이며 간단히 어떻게 사용할 것인지만 적어주면 됩니다. 밑에 시스템 유형은 일반으로 지정해주면 됩니다.

 

그리고 더 아래에는 일일 트래픽 1000을 허용하며 추후에 신청하여 늘릴 수 있다고 합니다.

이렇게 신청했다고 바로 API가 사용가능한 상황은 되지 않고 1~2시간 정도가 지나야 API를 사용할 수 있습니다.

 

4. API 테스트 해보기

우선 웹사이트상에서 미리보기를 확인할 수 있습니다. 저는 특정 노선 ID를 통해서 경우노선 전체를 확인해보고자 합니다. 110A 고려대 버스의 노선 정보가 궁금합니다. 이때 버스 노선 ID는 100100016입니다. 버스 노선 정보 역시 공공데이터포털에서 잘 검색하면 엑셀표로 된 것이 나오니까 찾아보시고 확인해보세요. 혹은 아래의 파일을 참고하시기 바랍니다.

서울시 버스노선ID 정보(20190508).csv
0.07MB

 

이것을 샘플데이터에 입력해주고 미리보기를 눌러보면 xml으로 정보를 잘 얻어오는 것을 확인해볼 수 있습니다.

 

 

 

API 사용해보기

 

1. 인터넷 사용권한 

 

AndroidManifest.xml에서 인터넷 사용권한을 지정해줍니다. 

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

 

2. Traffic 허용

그리고 다음과 같이 xml을 지정하여 우리가 사용하는 api 사이트의 traffic을 허용해주어야합니다. 아래와 같이 network_security_config.xml을 파일을 만든 후에 내용을 입력해주세요.

 

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">ws.bus.go.kr</domain>
    </domain-config>
</network-security-config>

 

이 파일을 다시 AndroidManifest.xml 파일에 지정해주어야 트래픽이 허용됩니다.

<application
        android:allowBackup="true"
        ...
        android:theme="@style/Theme.Chat"
        android:networkSecurityConfig="@xml/network_security_config">

 

3. HttpURLConnection으로 데이터 받기

네트워크를 사용할 것이니 Thread를 사용합니다. 아래는 공공데이터에서 제공하는 getArrInfoByRouteAll를 활용하는 자바 샘플 코드입니다. 


public class NetworkThread extends Thread{

    @Override
    public void run() {
        try {

            StringBuilder urlBuilder = new StringBuilder("http://ws.bus.go.kr/api/rest/arrive/getArrInfoByRouteAll");
            Log.e("MY_TEST","urlBuilder");
            urlBuilder.append("?" + URLEncoder.encode("ServiceKey", "UTF-8") + "=servicekey");
            urlBuilder.append("&" + URLEncoder.encode("busRouteId", "UTF-8") + "=" + URLEncoder.encode("100100016", "UTF-8"));
            URL url = new URL(urlBuilder.toString());
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Content-type", "application/json");
            BufferedReader rd;
            if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
                rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            } else {
                rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
            }
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = rd.readLine()) != null) {
                sb.append(line);
            }
            Log.e("BUS_API_TEST",sb.toString());
            rd.close();
            conn.disconnect();

        }catch(Exception e){
            e.printStackTrace();
        }

    }

}

 

위에서 보시면 ws.bus.go.kr이 우리가 트래픽을 허용한 주소라는 것을 알 수 있습니다. servicekey는 각자 자신이 받은 servicekey를 적어주시면 됩니다. 활용신청이 완료되었다면 인증키가 보일텐데 인코딩된 키를 이 부분에 사용하시면 됩니다.

 

이후 MainActivity에서는 그 쓰레드를 실행시키면 되지요.

public class MainActivity extends AppCompatActivity {
    private NetworkThread thread;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        thread=new NetworkThread();
        thread.start();
    }
}

 

이제 실행 후 확인해보면 정상적으로 값을 로그로 출력함을 확인해볼 수 있습니다.

 

이 후부터는 이제 xml 데이터를 파싱해서 알맞게 사용하시면 됩니다.

 

발생할 수 있는 오류

만약 SERVICE KEY IS NOT REGISTERED ERROR 가 발생했다면 등록한 Service ID가 틀렸거나 아직 서비스 ID가 등록되어있지 않았기 때문에 조금 더 기다렸다가 해보시기 바랍니다.

만약 응답을 아예 받아올 수 없는 경우에는 ws.bus.go.kr 에 트래픽 허용을 안해줘서 그렇습니다. 위 network_security_config.xml의 내용을 확인하여 주세요.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

RecyclerView로 수직 이동은 많이 해보았지만 수평이동을 하고 싶을때는 LinearLayoutManager를 아래와 같이 생성하면 된다. 기본적으로 LinearLayoutManager를 사용하면 수직 이동이다.

 

recyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL, false));

 

이렇게만 만들면 아이템을 양옆으로 드래그할때 아무 지점의 임의에 위치에서 멈추는데, 혹시 멈출때 어떤 하나의 아이템의 위치에서 멈추게 만들고 싶다면 SnapHelper의 객체를 이용하면 되는데 아래의 코드를 추가하면 된다.

recyclerView=(RecyclerView)findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL, false));
recyclerView.setAdapter(adapter);

SnapHelper snapHelper = new PagerSnapHelper();
if (recyclerView.getOnFlingListener() == null)
	snapHelper.attachToRecyclerView(recyclerView);

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  @Override
  public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
  	super.onScrollStateChanged(recyclerView, newState);
  }

  @Override
  public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    LinearLayoutManager layoutManager =
    LinearLayoutManager.class.cast(recyclerView.getLayoutManager());
  }
});

 

SnapHelper의 정확한 용도는 잘 모르므로 알아서 공부를 해야할 것 같다. 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

AsyncTask

1.5버전부터 추가된 AsyncTask는 개발자가 사용자 인터페이스에 대한 비동기적인 작업을 하게 해준다.

UI스레드에서 AsyncTask의 execute를 호출하면 되는데 이 AsyncTask를 물론 구현해주어야한다.

 

아래는 AsyncTask를 사용한 현재 시간 타이머


public class MainActivity extends AppCompatActivity {

    private TextView time;
    private int count=0;
    private SimpleDateFormat format=new SimpleDateFormat("yyyy년 MM월 dd일 a hh:mm:ss");
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        time=(TextView)findViewById(R.id.time);

        new AsyncCounterTask().execute();
    }

    class AsyncCounterTask extends AsyncTask<Void, Void, Integer> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            Log.e("__LOG__","onPreExecute");
        }
        @Override
        protected Integer doInBackground(Void... integers) {
            while(count<100){
                try {
                    Thread.sleep(1000);
                }catch(InterruptedException e){}
                //publishProgress 호출 후 바로 onProgressUpdate가 호출됨
                publishProgress();
            }
            Log.e("__LOG__","doInBackground End");
            return count;
        }

        @Override
        protected void onProgressUpdate(Void... values) {
            super.onProgressUpdate(values);
            Log.e("__LOG__","onProgressUpdate");
            long now = System.currentTimeMillis();
            time.setText(format.format(new Date(now)));
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            Log.e("__LOG__","onPostExecute");
        }
    }
}

 

AsyncTask<Params, Progress, Result>처럼 Generic을 사용한다.

 

Params : 실행 시 작업에 전달되는 값

Progress : 작업의 진행정도를 나타내는 값

Result : 작업의 결과를 나타내는 값

 

모두 사용할 필요는 없고 필요에 따라 사용하면 된다.

 

AsyncTask는 어떤 식으로 동작하게 될까? 다음은 몇안되는 AsyncTask의 메소드들인데 그냥 알아두면 편하다.

 

실행 순서는 onPreExecute -> doInBackground -> [ publishProgress -> onProgressUpdate ] ... -> onPostExecute

(위의 엑티비티를 실행하고 로그를 보면 알 수 있다. 아 참고로 접두사 pre는 ~전에, post는 ~ 후에 라는 점을 알면 기억하기 쉽겠다)

 

onPreExecute와 onPostExecute는 doInBackground 실행전, 실행후 한번씩만 실행이 된다. 그래서 doInBackground에서 반환하는 값이 onPostExecute에 전달이 된다. 

publishProgress를 호출하면 onProgressUpdate가 실행이 되는데 여기서 UI를 업데이트할 수 있다. 

 

여기서 중요한것은 doInBackground는 무조건 Override해야한다. (필자는 doInBackground가 쓰레드의 run과 같은 메소드와 비슷하다고 생각, 물론 자세히 까보면 다르겠지만....)

 

아는 사람은 알겠지만 SimpleDateFormat을 사용하여 날짜와 시간에 대한 format을 지정하고 System.currentMillis()를 이용하여 시스템의 현재 시간을 밀리초 단위(정확히 말하면 1970년 1월 1일부터 지금까지 흐른 밀리초)로 구하여 Date의 인자로 집어넣는다. 

이것을 format 형식으로 출력하려면 SimpleDateFormat의 format 메소드의 인자로 쏙 넣으면 되쥬.

 

그렇게 어려운 소스코드는 아니므로 이해가 될 거라 본다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

ViewPager, TabLayout

ViewPager는 화면을 양옆으로 밀어서 Page를 바꾸는 슬라이드 동작을 하는 View입니다. 굉장히 많이 사용하는 방식인데요. 보통 TabLayout과 같이 사용하는 것이 일반적입니다. 

아래처럼 1번, 2번과 같이 선택할 수 있게 하는 것이 TabLayout이며 프래그먼트를 포함하는 것이 ViewPager입니다. 물론 둘은 독립적으로도 사용할 수 있습니다.

 

아래와 같은 결과를 만들어내는것이 이번 포스팅의 목표입니다.

 

우선 구현하기에 앞서 TabLayout은 아래의 라이브러리가 있어야 사용할 수 있습니다. 추가합시다.

File - Project Structure - app - '+' - design 검색 후 아래 라이브러리 추가

1) activity_main.xml

MainActivity에서 사용할 activity_main에서는 TabLayout과 ViewPager를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


    <android.support.v4.view.ViewPager
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/viewPager"/>
</LinearLayout>

2) fragment1.xml, fragment2.xml

ViewPager에서 사용될 실제 내용을 담는 두 fragment의 layout입니다. 간단하게 TextView만이 존재하는 간단한 layout이네요.

 

fragment1.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:textSize="28dp"
        android:text="1번째 프래그먼트"/>
</LinearLayout>

fragment2.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:textSize="28dp"
        android:text="2번째 프래그먼트"/>
</LinearLayout>

 

3) Fragment1, Fragment2

위에서 정의한 layout을 inflate하는 Fragment1과 Fragment2입니다. 역시 아주 간단하게 onCreateView만 정의하였습니다.

 

Fragment1

package com.example.minkug.reakwonapp;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment1 extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        return inflater.inflate(R.layout.fragment1,container,false);
    }
}

Fragment2

package com.example.minkug.reakwonapp;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment2 extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        return inflater.inflate(R.layout.fragment2,container,false);
    }
}

 

 

 

4) PagerAdapter

가장 중요한 PagerAdapter를 구현할 차례입니다. 우서 가장 먼저할 것은 FragmentStatePagerAdapter를 상속을 받아야하지요.

이제 1개의 생성자와 2개의 메소드만 Overriding하면 기본적인 Adapter 작성은 끝이납니다.

 

4-1) FragmentManager를 받는 생성자 구현

4-2) 실제 Fragment를 반환하는 getItem 구현

4-3) 페이지의 개수를 반환하는 getCount구현 

 

여기서는 Adapter가 생성될때 fragments라는 list에 우리가 위에서 정의한 2개의 프래그먼트를 추가합니다. 그렇게 되면 getItem은 리스트의 i번재 아이템을 반환하면 될 것이고, getCount 역시 리스트의 사이즈를 반환하면 되기 때문에 구현이 조금 더 간단해집니다.

package com.example.minkug.reakwonapp;


import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;

import java.util.ArrayList;
import java.util.List;


public class PagerAdapter extends FragmentStatePagerAdapter {

    List<Fragment> fragments=new ArrayList<>();

    public PagerAdapter(FragmentManager fm) {
        super(fm);
        fragments.add(new Fragment1());
        fragments.add(new Fragment2());
    }

    @Override
    public Fragment getItem(int i) {
        return fragments.get(i);
    }

    @Override
    public int getCount() {
        return fragments.size();
    }
}

 

5) MainActivity

MainActivity가 코드가 조금 길어보이기는 하나 별다른 것이 없습니다. 우선 ViewPager, TabLayout을 얻어오고, TabLayout에는 Tab을 설정해주네요.

 

조금 눈여겨봐야될 것은 아래 2개의 Listener입니다.

5-1) viewPager.addOnPageChangeListener : Pager가 변경될때 발생하는 이벤트인데, 이때는 TabLayout의 탭까지 변경을 해줘야합니다. Pager를 슬라이딩하여 바꾼다고 하더라도 이 동작을 처리하지 않으면 Tab은 같이 변경되지 않습니다.


5-2) tabLayout.addOnTabSelectedListener : 마찬가지로 tab이 눌려졌다면 page도 같이 변경해주어야합니다. 탭이 선탤될때 발생하는 이벤트는 onTabSelected이며 tab이라는 인자로 선택된 tab의 위치를 알 수 있습니다. 이것을 이용해서 pager를 선택하면 되는 것이죠.

package com.example.minkug.reakwonapp;

import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;


public class MainActivity extends AppCompatActivity {

    private ViewPager viewPager;
    private TabLayout tabLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tabLayout=(TabLayout)findViewById(R.id.tabLayout);

        tabLayout.addTab(tabLayout.newTab().setText("1번"));
        tabLayout.addTab(tabLayout.newTab().setText("2번"));

        viewPager=(ViewPager)findViewById(R.id.viewPager);
        viewPager.setAdapter(new PagerAdapter(getSupportFragmentManager()));

        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
            }
            @Override
            public void onTabUnselected(TabLayout.Tab tab) { }
            @Override
            public void onTabReselected(TabLayout.Tab tab) { }

        });

    }
}

 

구현은 끝났고 여러분이 원하는 방향으로 기능을 추가해보시기 바랍니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

RecyclerView

RecyclerView는 ListView와 비슷하나 조금 더 진보한 View입니다. RecyclerView는 ListView에 비해서 많은 최적화를 거치게 됐고, 수 많은 데이터를 처리하는데 효과적입니다. 

 

그렇다면 RecyclerView가 어떻게 사용되는지 알아보도록 할까요? 여기서는 RecyclerView의 사용법을 위주로 설명할 것이기 때문에 디자인은 저 멀리 버리도록 하겠습니다.

 

1) RecyclerView를 사용하기 전에 라이브러리를 추가

File - Project Structure - App - Dependancies 의 오른쪽 +를 누른 후 recyclerview를 검색하여 

com.android.support:recyclerview를 추가합시다.

androidx를 사용하신다면 이 과정은 건너 뛰어도 상관없겠네요.

 

2) activity_main.xlm에 RecyclerView 추가

간단하게 RecyclerView만을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerView"/>

</LinearLayout>

 

3) item_recycler_view.xlm 추가

RecyclerView에 사용할 item입니다. 간단하게 TextView하나만을 포함하고 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="20dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        android:id="@+id/item"/>

</LinearLayout>

 

이제 준비는 끝났습니다. item_recycler_view를 Adapter의 ViewHolder에 이어주고 이 Adapter를 RecyclerView에 설정하면 됩니다.

 

4) MainActivity의 RecyclerView에 Adapter 설정

아직 RecyclerAdapter가 없기 때문에 오류가 발생하는데 우선 할당해놓고 이후에 Adapter class를 만들어봅시다. 

이후 딱히 설명할 건 없습니다.

몇가지 첨언한다면.. 

RecyclerView에 setLayoutManager는 어떤 레이아웃을 사용할 것인지 설정하는데, 여기서는 LinearLayoutManager를 사용했습니다. 

격자형을 사용하고 싶다면 GridLayoutManager를 사용하시기 바랍니다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView=findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(new RecyclerAdapter());

    }
}

 

 

 

 

 

5) RecyclerAdapter(Adapter class) 정의

우리는 우선 RecyclerView.Adapter를 상속을 받아야합니다.

 

5-1) ViewHolder 정의

Adpater안에는 반드시 ViewHolder가 있어야하는데 이 ViewHolder는 RecyclerView의 개별 Item의 대한 view를 가지고 있습니다. item_recycler_view가 이 ViewHolder의 View로 생성되며 필요에 따라 custom할 수 있겠죠? 

비록 우리는 item_recycler_view에 TextView만 추가했지만 뭐 ImageView도 추가할 수 있고 말이죠.

 

5-2) 메소드 Overriding

이제 필수적으로 Overriding할 메소드는 다음과 같습니다.

 

- onCreateViewHolder : ViewHolder를 콘텐츠를 표시하기 위해 사용하는 뷰를 설정하는데, 우리는 이전에 item_recycler_view를 만들었으니 이것을 사용합니다.

- onBindViewHolder : ViewHolder를 binding합니다. 여기서 ViewHolder의 각각 View를 설정할 수 있습니다.

- getItemCount : item의 갯수를 반환합니다. 대부분은 List를 사용하기 때문에 이 List의 size를 반환합니다.

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

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

    public RecyclerAdapter(){

        itemList.clear();

        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        private TextView item;
        public ViewHolder(View itemView){
            super(itemView);
            item=(TextView)itemView.findViewById(R.id.item);
        }
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view=LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {
        ViewHolder holder=(ViewHolder)viewHolder;

        holder.item.setText(itemList.get(i));
    }


    @Override
    public int getItemCount() {
        return itemList.size();
    }
}

 

ViewHolder를 정의하는데 여기서는 아까 만들었던 item_recycler_view.xlm의 View를 갖고 있습니다. 이 ViewHolder는 개별 item을 갖고 있는 것이나 같죠. 이 ViewHolder는 onCreateViewHolder에서 객체가 생성됩니다.

 

생성자에는 itemList를 초기화하는데 이 itemList에서 adapter가 binding할때 아이템을 꺼내옵니다. 우선 10개의 아이템을 추가하고 있네요.

이후 notifyDataSetChanged()를 호출하면 이제 데이터의 Set이 바뀌었다고 다시 RecyclerAdapter의 데이터를 표시하게됩니다. 

 

즉, itemList의 아이템이 생기거나 제거됐다면 이후 notifyDataSetChanged()를 호출하세요.

 

onBindViewHolder에는 ViewHolder에 있는 TextView의 문자열 설정을 하고 끝나네요.

 

여기까지 잘따라왔다면 프로젝트 구성은 이렇게 되겠네요.

 

 

 

결과

 

 

 

 

 

이벤트 추가

여기까지만 진행하면 조금 심심하겠죠? 이제 아이템이 클릭이 되면 Toast로 클릭되었다는 메시지를 띄어봅시다.

interface를 사용하여 listener를 설정할때가 왔군요.

 

6) RecyclerAdapter에 OnItemClickListener 설정

여기서는 OnItemClickListener라는 interface를 정의하고 객체를 생성합니다.

onBindViewHolder에서 ViewHolder의 아이템이 클릭되었을때 onItemClickListener의 onClick 메소드를 호출합니다. 이 onClick메소드는 바로 MainActivity에서 구현하거나 객체 생성해서 넘길테지요.

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<String> itemList=new ArrayList<>();
    public interface OnItemClickListener{
        public void onClick(String str);
    }

    private OnItemClickListener onItemClickListener=null;

    public RecyclerAdapter(){

        itemList.clear();

        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        private TextView item;
        public ViewHolder(View itemView){
            super(itemView);
            item=(TextView)itemView.findViewById(R.id.item);
        }
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view=LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {
        ViewHolder holder=(ViewHolder)viewHolder;

        holder.item.setText(itemList.get(i));
        holder.item.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onItemClickListener!=null){
                    onItemClickListener.onClick(itemList.get(i)+" 클릭!!");
                }
            }
        });
    }


    @Override
    public int getItemCount() {
        return itemList.size();
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener){
        this.onItemClickListener=onItemClickListener;
    }
}

 

 

7) MainActivity에서 onClick 구현

별로 특별한 기능은 하지 않고 onClick시 Toast를 이용해 메시지를 보여줍니다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView=findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        RecyclerAdapter adapter=new RecyclerAdapter();
        recyclerView.setAdapter(adapter);

        adapter.setOnItemClickListener(new RecyclerAdapter.OnItemClickListener() {
            @Override
            public void onClick(String str) {
                Toast.makeText(MainActivity.this,str,Toast.LENGTH_LONG).show();
            }
        });
    }
}

 

인터페이스를 사용하여 MainActivity에서 Toast 메시지를 실행시킨 이유는 Activity에서만 사용할 수 있는 메소드를 사용하기 위해서입니다(예를 들면 startActivity).

물론 RecyclerAdapter에 MainActivity의 Context를 전달해서 구현할 수도 있었지요. 

 

이제 아이템을 누르면 아래와 같이 메시지가 나타납니다.

 

 

 

특정 위치에 다른 View 설정

맨 처음 또는 특정 position에 다른 item을 할당하고 싶다면 어떻게 해야할까요?

그 경우에는 getItemViewType를 Overriding하여 해결할 수 있습니다.

getItemViewType은 인자로 전달된 position에 따라 ViewType이 무엇인지 결정하고 

onCreateViewHolder에 두번째 인자로 그 ViewType으로 전달됩니다. onCreateViewHolder는 그 뷰타입을 보고 생성되는 ViewHolder를 결정하면 되지요.

 

저는 0번째 item만 다른 view로 보여주려고 합니다.

1) item_header.xml

우선 0번째 아이템을 위해 사용할 layout은 다음과 같습니다. 아주 간단하지요.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/header_msg"
        android:textAlignment="center"
        android:paddingTop="20dp"
        android:paddingBottom="20dp"/>
</LinearLayout>

 

2) RecyclerAdapter에 HeaderViewHolder 추가

필요한 부분만 간추려서 보면 아래의 코드와 같습니다. 우선 HeaderViewHolder라는 0번째 아이템을 위한 ViewHolder를 추가하지요. 

 

0번째 아이템이면 TYPE_HEADER라는 ViewType을, 그 외에는 평범한 아이템을 나타내는 TYPE_ITEM이라는 것을 알려주는 getItemViewType을 오버라이딩하고 있네요.

 

이제 이 ViewType은 onCreateViewHolder의 두번째 인자인 i로 전달됩니다. 여기서 ViewHolder를 두 가지로 나눠서 반환하게 됩니다. HeaderViewHolder 또는 ViewHolder(RecyclerAdapter에서 정의한)로 말입니다.

 

이후 onBindViewHolder에서 각각 다른 처리를 하면 됩니다.

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
	...
    private final int TYPE_HEADER=0;
    private final int TYPE_ITEM=1;

    public RecyclerAdapter(){
        itemList.clear();

        itemList.add("Header Message");
        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

   ...
    class HeaderViewHolder extends RecyclerView.ViewHolder{
        private TextView headerItem;
        public HeaderViewHolder(View itemView){
            super(itemView);
            headerItem=(TextView)itemView.findViewById(R.id.header_msg);
        }
    }

    @Override
    public int getItemViewType(int position) {
        if(position==0) return TYPE_HEADER;
        return TYPE_ITEM;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        if(i==TYPE_HEADER){
            return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.item_header,viewGroup,false));
        }

        return new ViewHolder(LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false));
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {

        if(i==0){
            ((HeaderViewHolder) viewHolder).headerItem.setText(itemList.get(i));
            return;
        }
        ((ViewHolder) viewHolder).item.setText(itemList.get(i));
    }
	...
}

 

이후 결과는 아래와 같습니다. 0번째 아이템이 다른 view를 가지고 있지요.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,