안드로이드 앱이 PHP 프로그램을 매개로 하여 MySQL 데이터베이스 서버에 데이터를 JSON 형식으로 가져오는 간단한 예제입니다.
1. 웹브라우저로 PHP 동작 테스트
2. Android 앱에서 테스트
3. 코드 설명
4. 관련 포스팅
5. 참고
|
최초 작성 2015. 11. 22
최종 작성 2019. 11. 17 androidx 사용하도록 변경
안드로이드 앱이 웹서버에 있는 PHP 파일 실행시키면, PHP 코드는 질의를 하여 MySQL 서버로부터 데이터를 가져옵니다.
그리고 나서 데이터를 JSON 포맷으로 가공하여 안드로이드 앱으로 넘겨줍니다.
안드로이드 앱은 JSON 포맷의 데이터를 파싱하여 원하는 UI에 데이터를 보기 좋게 보여주게 됩니다.
JSON(제이슨, JavaScript Object Notation)은 사람이 읽을 수 있는 텍스트를 사용하여 데이터를 전달할 수 있도록 해주는 개방형 표준 포맷입니다.
데이터를 키(key)와 값(value)가 쌍을 이루는 형식을 변환하여 사용합니다.
다음 포스팅에 연결된 포스팅입니다.
Amazon AWS를 사용하여 구현한 예제입니다.
AWS Rest API를 Android와 연동해보기( Lambda + API Gateway + DynamoDB )
https://webnautes.tistory.com/1590
1. 웹브라우저로 PHP 동작 테스트
Android 앱으로 테스트를 진행하기 전에 다음 PHP 코드를 웹 브라우저로 간단한 테스트를 해보겠습니다.
1-1. 앞에서 진행했던 포스팅의 결과로 현재 testdb 데이터베이스의 person 테이블에 데이터가 저장되어 있는 상태입니다.
다음 두 개의 파일이 필요합니다.
dbcon.php
MySQL 서버 접속을 위해 사용되는 코드입니다. 위에서 언급한 포스팅에 있습니다.
getjson.php
MySQL에 질의를 보내어 결과를 가져오고 JSON 포맷으로 변환하여 출력하는 역할을 합니다.
getjson.php
<?php
error_reporting(E_ALL); ini_set('display_errors',1);
include('dbcon.php');
$stmt = $con->prepare('select * from person'); $stmt->execute();
if ($stmt->rowCount() > 0) { $data = array();
while($row=$stmt->fetch(PDO::FETCH_ASSOC)) { extract($row); array_push($data, array('id'=>$id, 'name'=>$name, 'country'=>$country )); }
header('Content-Type: application/json; charset=utf8'); $json = json_encode(array("webnautes"=>$data), JSON_PRETTY_PRINT+JSON_UNESCAPED_UNICODE); echo $json; }
?> |
1-2. 윈도우라면 C:\wamp64\www\ 경로에 파일을 생성합니다.
1-3. 우분투라면 /var/www/html/ 경로에 파일을 생성합니다.
1-4. 웹브라우저에서 localhost/getjson.php 주소에 접속하면 캡쳐화면처럼 JSON 형식으로 데이터를 보여줍니다.
2. Android 앱에서 테스트
웹서버의 PHP 파일을 매개로하여 데이터베이스의 데이터를 가져오는 안드로이드 앱을 작성합니다.
2-1. 매니페스트 파일 AndroidManifest.xml의 manifest 태그 하위 항목으로 인터넷 접근 허용 퍼미션을 추가합니다.
android 9.0 이상에서 동작하게 하려면 usesCleartextTraffic 옵션도 추가해야 합니다. 해주지 않으면 “cleartext http traffic to not permitted” 라는 에러가 발생합니다.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tistory.webnautes.phptest">
<uses-permission android:name="android.permission.INTERNET" />
<application android:usesCleartextTraffic="true" android:allowBackup="true" |
2-2. 레이아웃 파일 activity_main.xml을 아래 내용으로 대체합니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity" android:layout_margin="15dp" android:padding="10dp">
<androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="6" android:layout_margin="5dp" android:padding="5dp" android:id="@+id/listView_main_list" />
<Button android:layout_margin="2dp" android:id="@+id/button_main_all" android:layout_width="match_parent" android:layout_weight="2" android:layout_height="0dp" android:text="전체보기" />
<TextView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="4" android:layout_margin="5dp" android:padding="5dp" android:id="@+id/textView_main_result" />
</LinearLayout> |
상단의 RecyclerView에는 데이터베이스로부터 가져온 JSON 형식의 데이터를 파싱해서 항목별로 보여줍니다.
하단의 TextView에는 JSON 형식의 데이터 또는 각종 에러를 보여주는 용도로 사용됩니다.
2-3. 레이아웃 파일 item_list.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" android:orientation="horizontal" android:layout_margin="5dp" android:padding="5dp">
<TextView android:id="@+id/textView_list_id" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" />
<TextView android:id="@+id/textView_list_name" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" />
<TextView android:id="@+id/textView_list_country" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" />
</LinearLayout> |
RecyclerView의 한 줄을 여러 개의 열로 표현하기 위해 필요한 레이아웃입니다.
필요한 열 개수 만큼 TextView를 추가하고 보여주는 데이터 값에 맞게 크기를 지정해주면 됩니다.
2-4. RecyclerView를 사용하려면 build.gradle에 다음 노란줄을 추가해야 합니다.
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.recyclerview:recyclerview:1.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' }
|
2-5. 자바 파일 MainActivity.java을 아래 내용으로 바꿉니다.
다음 줄에 있는 IP 주소를 아파치 웹서버가 설치된 컴퓨터의 IP로 수정하세요.
private static String IP_ADDRESS = "IP주소";
안드로이드 에뮬레이터와 서버가 같은 컴퓨터에 동작하는 경우에는 다음 아이피를 입력합니다.
Android Studio의 에뮬레이터 - 10.0.2.2
GenyMotion - 192.168.56.1
package com.tistory.webnautes.phptest;
import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView;
import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private static String IP_ADDRESS = "IP주소"; private static String TAG = "phptest";
private EditText mEditTextName; private EditText mEditTextCountry; private TextView mTextViewResult; private ArrayList<PersonalData> mArrayList; private UsersAdapter mAdapter; private RecyclerView mRecyclerView; private EditText mEditTextSearchKeyword; private String mJsonString;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
mTextViewResult = (TextView)findViewById(R.id.textView_main_result); mRecyclerView = (RecyclerView) findViewById(R.id.listView_main_list); mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mTextViewResult.setMovementMethod(new ScrollingMovementMethod());
mArrayList = new ArrayList<>();
mAdapter = new UsersAdapter(this, mArrayList); mRecyclerView.setAdapter(mAdapter);
Button button_all = (Button) findViewById(R.id.button_main_all); button_all.setOnClickListener(new View.OnClickListener() { public void onClick(View v) {
mArrayList.clear(); mAdapter.notifyDataSetChanged();
GetData task = new GetData(); task.execute( "http://" + IP_ADDRESS + "/getjson.php", ""); } });
}
private class GetData extends AsyncTask<String, Void, String>{
ProgressDialog progressDialog; String errorString = null;
@Override protected void onPreExecute() { super.onPreExecute();
progressDialog = ProgressDialog.show(MainActivity.this, "Please Wait", null, true, true); }
@Override protected void onPostExecute(String result) { super.onPostExecute(result);
progressDialog.dismiss(); mTextViewResult.setText(result); Log.d(TAG, "response - " + result);
if (result == null){
mTextViewResult.setText(errorString); } else {
mJsonString = result; showResult(); } }
@Override protected String doInBackground(String... params) {
String serverURL = params[0]; String postParameters = params[1];
try {
URL url = new URL(serverURL); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setReadTimeout(5000); httpURLConnection.setConnectTimeout(5000); httpURLConnection.setRequestMethod("POST"); httpURLConnection.setDoInput(true); httpURLConnection.connect();
OutputStream outputStream = httpURLConnection.getOutputStream(); outputStream.write(postParameters.getBytes("UTF-8")); outputStream.flush(); outputStream.close();
int responseStatusCode = httpURLConnection.getResponseCode(); Log.d(TAG, "response code - " + responseStatusCode);
InputStream inputStream; if(responseStatusCode == HttpURLConnection.HTTP_OK) { inputStream = httpURLConnection.getInputStream(); } else{ inputStream = httpURLConnection.getErrorStream(); }
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder sb = new StringBuilder(); String line;
while((line = bufferedReader.readLine()) != null){ sb.append(line); }
bufferedReader.close();
return sb.toString().trim();
} catch (Exception e) {
Log.d(TAG, "GetData : Error ", e); errorString = e.toString();
return null; }
} }
private void showResult(){
String TAG_JSON="webnautes"; String TAG_ID = "id"; String TAG_NAME = "name"; String TAG_COUNTRY ="country";
try { JSONObject jsonObject = new JSONObject(mJsonString); JSONArray jsonArray = jsonObject.getJSONArray(TAG_JSON);
for(int i=0;i<jsonArray.length();i++){
JSONObject item = jsonArray.getJSONObject(i);
String id = item.getString(TAG_ID); String name = item.getString(TAG_NAME); String country = item.getString(TAG_COUNTRY);
PersonalData personalData = new PersonalData();
personalData.setMember_id(id); personalData.setMember_name(name); personalData.setMember_country(country);
mArrayList.add(personalData); mAdapter.notifyDataSetChanged(); }
} catch (JSONException e) {
Log.d(TAG, "showResult : ", e); }
}
} |
2-6. PersonalData.java 파일을 추가합니다.
MainActivity와 UsersAdapter에서 사용하는 클래스로 각각 다음 용도로 사용합니다.
- MainActivity- ArrayList에 데이터를 저장하기 위해 사용됩니다.
- UsersAdapter - ArrayList에 있는 데이터를 RecyclerView에 보여줄 때 사용됩니다.
package com.tistory.webnautes.phptest;
public class PersonalData { private String member_id; private String member_name; private String member_country;
public String getMember_id() { return member_id; }
public String getMember_name() { return member_name; }
public String getMember_country() { return member_country; }
public void setMember_id(String member_id) { this.member_id = member_id; }
public void setMember_name(String member_name) { this.member_name = member_name; }
public void setMember_country(String member_address) { this.member_country = member_address; } } |
2-7. UsersAdapter.java 파일을 추가합니다.
ArrayList에 있는 PersonalData 타입의 데이터를 RecyclerView에 보여주는 작업을 합니다.
package com.tistory.webnautes.phptest;
import android.app.Activity; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView;
import java.util.ArrayList;
public class UsersAdapter extends RecyclerView.Adapter<UsersAdapter.CustomViewHolder> {
private ArrayList<PersonalData> mList = null; private Activity context = null;
public UsersAdapter(Activity context, ArrayList<PersonalData> list) { this.context = context; this.mList = list; }
class CustomViewHolder extends RecyclerView.ViewHolder { protected TextView id; protected TextView name; protected TextView country;
public CustomViewHolder(View view) { super(view); this.id = (TextView) view.findViewById(R.id.textView_list_id); this.name = (TextView) view.findViewById(R.id.textView_list_name); this.country = (TextView) view.findViewById(R.id.textView_list_country); } }
@Override public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_list, null); CustomViewHolder viewHolder = new CustomViewHolder(view);
return viewHolder; }
@Override public void onBindViewHolder(@NonNull CustomViewHolder viewholder, int position) {
viewholder.id.setText(mList.get(position).getMember_id()); viewholder.name.setText(mList.get(position).getMember_name()); viewholder.country.setText(mList.get(position).getMember_country()); }
@Override public int getItemCount() { return (null != mList ? mList.size() : 0); }
} |
2-8. 실행결과 입니다.
상단 RecyclerView는 MySQL에서 가져온 데이터를 파싱해서 리스트 형태로 출력합니다.
하단 TextView는 파싱 전 JSON 형식의 데이터 또는 에러를 출력합니다.
3. 코드 설명
1-1. PersonalData.java 파일에 사용할 데이터가 선언되어 있습니다.
public class PersonalData { private String member_id; private String member_name; private String member_country;
} |
데이터 추가시 클래스의 데이터 저장 및 읽어올때 사용되는 멤버 메소드를 추가하는 방법은 다음 포스팅을 참고하세요.
1-2. UsersAdapter.java 파일에 데이터를 보여주기 위한 처리가 있습니다.
레이아웃 파일에 있는 UI 컴포넌트를 CustomViewHolder 클래스의 멤버변수와 연결합니다.
class CustomViewHolder extends RecyclerView.ViewHolder { protected TextView id; protected TextView name; protected TextView country;
public CustomViewHolder(View view) { super(view); this.id = (TextView) view.findViewById(R.id.textView_list_id); this.name = (TextView) view.findViewById(R.id.textView_list_name); this.country = (TextView) view.findViewById(R.id.textView_list_country); } } |
onBindViewHolder 호출될때 CustomViewHolder에 데이터를 추가합니다.
@Override public void onBindViewHolder(@NonNull CustomViewHolder viewholder, int position) {
viewholder.id.setText(mList.get(position).getMember_id()); viewholder.name.setText(mList.get(position).getMember_name()); viewholder.country.setText(mList.get(position).getMember_country()); } |
3. 버튼을 클릭시 GetData AsyncTask가 실행됩니다.
안드로이드 코드에서 실행시킬 서버의 IP_ADDRESS와 PHP 파일 이름을 지정해줍니다.
Button button_all = (Button) findViewById(R.id.button_main_all); button_all.setOnClickListener(new View.OnClickListener() { public void onClick(View v) {
mArrayList.clear(); mAdapter.notifyDataSetChanged();
GetData task = new GetData(); task.execute( "http://" + IP_ADDRESS + "/getjson.php", ""); } }); |
4. doInBackground 메소드에서 서버에 있는 PHP 파일을 실행시키고 응답을 저장하고 스트링으로 변환하여 리턴합니다.
@Override protected String doInBackground(String... params) { |
다음 포스팅에서 설명한 부분이라 설명을 생략합니다.
5. 에러가 있는 경우 에러메시지를 보여주고 아니면 JSON을 파싱하여 화면에 보여주는 showResult 메소드를 호출합니다.
@Override protected void onPostExecute(String result) { super.onPostExecute(result);
progressDialog.dismiss(); mTextViewResult.setText(result); Log.d(TAG, "response - " + result);
if (result == null){
mTextViewResult.setText(errorString); } else {
mJsonString = result; showResult(); } } |
6. 안드로이드 앱에서 받은 JSON 포맷의 데이터입니다.
간단한 규칙이 있는데 중괄호 {} 는 JSONObject 대괄호 [] JSONArray입니다.
{ "webnautes": [ { "id": "1", "name": "홍길동", "country": "조선" }, { "id": "2", "name": "잔다르크", "country": "프랑스" } ] } |
7. 첫번째 괄호는 중괄호 {} 이므로 JSONObject입니다.
JSONObject jsonObject = new JSONObject(mJsonString); |
8. jsonObject에서 TAG_JSON 키를 갖는 JSONArray를 가져옵니다.
JSONArray jsonArray = jsonObject.getJSONArray(TAG_JSON); |
이제 다음같은 구조를 jsonArray가 갖고 있게 됩니다.
[ { "id": "1", "name": "홍길동", "country": "조선" }, { "id": "2", "name": "잔다르크", "country": "프랑스" } ] |
9. JsonArray에는 JSONObject가 데이터 갯수만큼 포함되어 있습니다.
인덱스를 사용하여 JsonArray에서 JSONObject를 하나씩 가져옵니다.
for(int i=0;i<jsonArray.length();i++){ JSONObject item = jsonArray.getJSONObject(i); |
10. JSONObject에서 키 id, name, country의 값을 가져옵니다.
String id = item.getString(TAG_ID); String name = item.getString(TAG_NAME); String country = item.getString(TAG_COUNTRY); |
11. 데이터를 새로 생성한 PersonalData 클래스의 멤버변수에 입력하고 ArrayList에 추가합니다.
PersonalData personalData = new PersonalData();
personalData.setMember_id(id); personalData.setMember_name(name); personalData.setMember_country(country);
mArrayList.add(personalData); |
12. 리스트에 데이터가 변경되었음을 알려줍니다. 화면에 추가된 데이터들이 보입니다.
mAdapter.notifyDataSetChanged(); |
4. 관련 포스팅
5. 참고
[1] http://codeigniter-kr.org/bbs/view/tip?idx=8300
[2] http://www.simplifiedcoding.net/android-json-parsing-retrieve-from-mysql-database/
[3] http://www.simplifiedcoding.net/android-json-tutorial-to-get-data-from-mysql-database/