앱 개발자라면 사용자에게 다양한 데이터를 제공하기 위해 웹 API를 연동해야 하는 경우가 자주 있습니다. 다행히 생각만큼 어렵지 않으며, XML 또는 JSON 형식으로 구현할 수 있습니다. 이 튜토리얼에서는 안드로이드 앱에 간단한 웹 API 데이터 가져오기(fetcher) 기능을 구축하는 방법을 단계별로 살펴보겠습니다.
이 튜토리얼에서는 XML 대신 JSON을 사용합니다.
JSON으로 코드 작성하기
먼저 새로운 리스트 뷰 XML 파일을 만들고 아래 코드를 입력합니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <ListView android:id="@+id/list_title_list" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </LinearLayout>
다음으로 또 하나의 XML 파일을 생성합니다. 이 파일에는 화면에 표시할 요소들(ImageView, TextView 등)을 정의합니다.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="80dp" android:orientation="vertical" android:padding="5dp" > <ImageView android:id="@+id/iv_icon_social" android:layout_width="60dp" android:layout_height="60dp" android:layout_centerVertical="true" android:visibility="gone" /> <LinearLayout android:id="@+id/thumbnail" android:layout_width="fill_parent" android:layout_height="85dp" android:layout_marginRight="50dp" android:layout_marginTop="0dp" android:layout_toRightOf="@+id/iv_icon_social" android:gravity="center_vertical" android:orientation="vertical" android:padding="5dip" android:visibility="visible" > <TextView android:id="@+id/txt_ttlsm_row" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingLeft="10dp" android:text="Sample text" android:textSize="18dp" android:textStyle="bold" /> <TextView android:id="@+id/txt_ttlcontact_row2" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="0dp" android:layout_marginTop="3dp" android:paddingLeft="10dp" android:maxEms="20" android:maxLines="2" android:singleLine="false" android:ellipsize="end" android:text="Sample text2" android:textColor="#808080" android:textSize="15dp" android:textStyle="normal" /> </LinearLayout> </RelativeLayout>
AsyncTask로 API 데이터 가져오기
API에서 데이터를 가져오려면 메인 액티비티로 이동한 뒤 AsyncTask를 사용해야 합니다. API 호출은 메인 스레드에서 수행할 수 없기 때문입니다. AsyncTask는 백그라운드 프로세스를 처리하면서 그 결과를 UI에 표시해 주는 스레드입니다. 네트워크 작업을 메인 스레드에서 실행하면 대개 앱이 비정상 종료됩니다. 이 부분이 다소 혼란스럽다면 RapidAPI에서 제공하는 심화 API 튜토리얼을 참고하는 것도 좋은 방법입니다.
class DownloadFilesTask extends AsyncTask < Void, Void, String >
{
private final ProgressDialog dialog = new ProgressDialog(MainActivity.this);
@Override
protected void onPreExecute()
{
super.onPreExecute();
this.dialog.setMessage("Signing in...");
this.dialog.show();
}
@Override
protected String doInBackground(Void...params)
{
ServiceHandler sh = new ServiceHandler();
String jsonStr = sh.makeServiceCall("https://jsonplaceholder.typicode.com/albums/", ServiceHandler.GET);
Log.d("res1", jsonStr);
return jsonStr;
}
@Override
protected void onPostExecute(String response)
{
super.onPostExecute(response);
Log.d("res2", response);
dialog.dismiss();
if (response != null)
{
try
{
JSONArray arr = new JSONArray(response);
DataModel mDatModel = new DataModel();
for (int i = 0; i < arr.length(); i++)
{
JSONObject c = arr.getJSONObject(i);
String id=c.getString(ID);
String title = c.getString(TITLE);
String uid = c.getString(USER_ID);
id_array.add(id);
// Toast.makeText(getApplicationContext(), title, Toast.LENGTH_LONG).show();
}
adapter = new AAdapter(MainActivity.this, id_array);
l.setAdapter(adapter);
}
catch (Exception e)
{}
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener
{
AAdapter adapter;
ArrayList < String > id_array = new ArrayList < String > ();
ArrayList < String > notice_array = new ArrayList < String > ();
Button b1;
ListView l;
private final static String SERVICE_URI = "https://jsonplaceholder.typicode.com/albums/";
private static final String TAG_QUESTIONS = "Questions";
private static final String USER_ID = "userId";
private static final String ID = "id";
private static final String TITLE = "title";
JSONArray questions = null;
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
}
protected void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
l = (ListView) findViewById(R.id.list);
new DownloadFilesTask().execute();
l.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView < ? > parent, View view, int position, long id)
{
String abc = id_array.get(position);
Toast.makeText(getBaseContext(), id_array.get(position), Toast.LENGTH_LONG).show();
Intent n = new Intent(MainActivity.this, Titleshow.class);
n.putExtra("id", id_array.get(position));
startActivity(n);
}
});
}
@Override
public void onItemClick(AdapterView < ? > parent, View view, int position, long id)
{}메인 액티비티에서 API 응답을 확인할 수 있습니다.
ServiceHandler 클래스
아래의 ServiceHandler는 웹 API에 요청을 보내고 응답을 받아오는 역할을 담당하는 서비스 핸들러 클래스입니다. GET과 POST 두 가지 HTTP 요청 방식을 모두 지원합니다.
ServiceHandler sh = new ServiceHandler();
String jsonStr = sh.makeServiceCall("https://jsonplaceholder.typicode.com/albums/", ServiceHandler.GET);
this nothing but a service handle class to request and get the respose from web api so we will do all this from web api.
ServiceHandler.java
class ServiceHandler
{
static String response = null;
public final static int GET = 1;
public final static int POST = 2;
public ServiceHandler()
{}
/**
* Making service call
*
* @url - url to make request
* @method - http request method
*/
public String makeServiceCall(String url, int method)
{
return this.makeServiceCall(url, method, null);
}
/**
* Making service call
*
* @url - url to make request
* @method - http request method
* @params - http request params
*/
public String makeServiceCall(String url, int method, List < NameValuePair > params)
{
try
{
// http client
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpEntity httpEntity = null;
HttpResponse httpResponse = null;
// Checking http request method type
if (method == POST)
{
HttpPost httpPost = new HttpPost(url);
// adding post params
if (params != null)
{
httpPost.setEntity(new UrlEncodedFormEntity(params));
}
httpResponse = httpClient.execute(httpPost);
}
else if (method == GET)
{
// appending params to url
if (params != null)
{
String paramString = URLEncodedUtils.format(params, "utf-8");
url += "?" + paramString;
}
HttpGet httpGet = new HttpGet(url);
httpResponse = httpClient.execute(httpGet);
}
httpEntity = httpResponse.getEntity();
response = EntityUtils.toString(httpEntity);
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
catch (ClientProtocolException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
return response;
}
}커스텀 어댑터(Adapter) 만들기
다음으로 AsyncTask 내부에서 사용할 어댑터 클래스가 필요합니다. setAdapter 메서드를 사용하려면 커스텀 어댑터를 직접 만들어야 합니다.
public class AAdapter extends BaseAdapter
{
private Activity activity;
// private ArrayList<HashMap<String, String>> data;
private static ArrayList title;
private static LayoutInflater inflater = null;
public AAdapter(Activity a, ArrayList b)
{
activity = a;
this.title = b;
inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public int getCount()
{
return title.size();
}
public Object getItem(int position)
{
return position;
}
public long getItemId(int position)
{
return position;
}
public View getView(int position, View convertView, ViewGroup parent)
{
View vi = convertView;
if (convertView == null) vi = inflater.inflate(R.layout.abcd, null);
TextView title2 = (TextView) vi.findViewById(R.id.txt_ttlsm_row); // title
String song = title.get(position).toString();
title2.setText(song);
TextView title22 = (TextView) vi.findViewById(R.id.txt_ttlcontact_row2); // notice
String song2 = title.get(position).toString();
title22.setText(song2);
return vi;
}
}안드로이드에서 어댑터 클래스는 추상 클래스로 취급되므로 모든 메서드를 반드시 구현해야 합니다. 그중에서도 getView() 메서드가 특히 중요한데, 여기에 추가 코드를 작성하여 두 번째 XML 레이아웃 파일을 화면에 표시하게 됩니다.
inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
이미지 로더용 어댑터
API에서 가져온 데이터에 이미지가 포함되어 있다면, 인터넷에서 이미지를 불러올 수 있는 어댑터 클래스도 추가로 필요합니다.
public class MyAdapter extends BaseAdapter
{
ImageLoader imageLoader;
private Context ctx;
ArrayList < AlbumData > list;
public MyAdapter(Context ctx, ArrayList < AlbumData > mDataList)
{
list = mDataList;
this.ctx = ctx;
}
public int getCount()
{
return list.size();
}
public Object getItem(int position)
{
return position;
}
public long getItemId(int position)
{
return position;
}
public View getView(int position, View convertView, ViewGroup parent)
{
// View vi = convertView;
ViewHolder viewHolder;
if (convertView == null)
{
LayoutInflater vi = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = vi.inflate(R.layout.abcde, null);
viewHolder = new ViewHolder();
viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
viewHolder.textView = (TextView) convertView.findViewById(R.id.txt_ttlsm_row);
convertView.setTag(viewHolder);
}
else
{
viewHolder = (ViewHolder) convertView.getTag();
}
AlbumData data = list.get(position);
String a = data.getThumbnailUrl();
if (data != null)
{
viewHolder.textView.setText(data.getTitle());
String url = a;
Glide.with(ctx).load(url).centerCrop().placeholder(R.drawable.abhi).crossFade().into(viewHolder.imageView);
/* Picasso.with(ctx)
.load("https://preview.keenthemes.com/conquer/assets/plugins/jcrop/demos/demo_files/image2.jpg")
.into(viewHolder.imageView);*/
/* ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.displayImage("https://www.keenthemes.com/preview/conquer/assets/plugins/jcrop/demos/demo_files/image1.jpg", viewHolder.imageView);*/
}
return convertView;
}
public class ViewHolder
{
ImageView imageView;
TextView textView;
}코드 주석에는 Picasso 이미지 로더 사용 예시도 함께 담았습니다. Picasso는 안드로이드를 위한 강력한 오픈소스 이미지 다운로더이자 캐싱 라이브러리로, 앱에서 가벼운 용량을 유지할 수 있다는 장점이 있습니다. 반면 GIF 지원, 더 나은 메모리 관리, 폭넓은 커스터마이징이 필요하다면 Glide 이미지 로더를 사용하는 것이 좋습니다.
ViewHolder 패턴
public class ViewHolder
{
ImageView imageView;
TextView textView;
}ViewHolder 클래스는 ViewHolder의 정적(static) 인스턴스를 생성하여 첫 로딩 시 뷰 항목에 연결합니다. 이후 호출에서는 뷰 태그(tag)에서 해당 인스턴스를 꺼내 재사용합니다.
getView() 메서드는 ListView에 항목이 많을 때처럼 매우 자주 호출됩니다. 따라서 setter와 getter 메서드를 담은 데이터 모델 클래스를 함께 구현해 두면 배열 리스트의 요소를 손쉽게 다룰 수 있어 이후 개발 과정이 한결 편리해집니다.
데이터 모델 클래스
public class AlbumData
{
int albumId;
int id;
String title;
String url;
String thumbnailUrl;
public int getAlbumId()
{
return albumId;
}
public void setAlbumId(int albumId)
{
this.albumId = albumId;
}
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public String getUrl()
{
return url;
}
public void setUrl(String url)
{
this.url = url;
}
public String getThumbnailUrl()
{
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl)
{
this.thumbnailUrl = thumbnailUrl;
}
}마무리 및 테스트
지금까지 소개한 내용만으로 기본적인 구현에 필요한 모든 요소를 갖추었습니다. 완성된 API 데이터 가져오기 기능은 BlueStacks 같은 앱 에뮬레이터에서 출력 결과를 테스트해 볼 수 있습니다.