Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

집요정 도비의 일기

Step 2. 음악 스트리밍 서버(Spring boot), 앱(Android)을 만들어보자. 본문

개발 일기

Step 2. 음악 스트리밍 서버(Spring boot), 앱(Android)을 만들어보자.

집요정_도비 2016. 3. 3. 12:12

* 저번 포스트에 이어서..


6. 목적

* 저번에 서버와 클라이언트간의 통신이 잘 됨을 확인하였으므로, 이번엔 서버의 특정 디렉토리에서 파일 목록을 불러와서 클라이언트에서 확인 할 수 있도록 해봄.

(1) 서버

- 디렉토리에 대한 요청이 오면, 이를 처리하여 안드로이드에 돌려줄 수 있어야 한다.

- 서버는 디렉토리의 내용물들이 파일인지(.mp3 파일만을 확인하고 나머지는 버린다), 디렉토리인지 확인하고, 이를 클라이언트도 구분할 수 있게 해줘야한다.


(2) 클라이언트(안드로이드)

- 서버에 특정 디렉토리, 혹은 그 디렉토리의 하위 디렉토리의 내용물에 대해서 정보를 요청하고, 이를 RecyclerView를 통해 화면에 뿌려줄 수 있다.


7-1. 서버(Spring Boot) 구현


요청 받은 디렉토리 밑의 파일들의 정보를 저장할 객체를 만든다. 단순히 파일의 이름(string)과 파일인지 디렉토리인지 확인할 값(boolean)만 갖는 단순한 객체이다.

* FileItem.java



이제 RequestController.java를 수정해봄.

우선 음악 파일이 있는 디렉토리를 지정함. 프로그램 시작 시, 파라미터로 받아서 할 수도 있으나 그냥 하드코딩함.


public static String FOLDER_PATH = "F:\\Music\\";


그리고 본격적으로 이를 활용한 컨트롤러 메소드를 작성함.


* RequestController.java



*파라미터 : String name은 해당 디렉토리의 이름이다. 만약, 기본 디렉토리를 요청하는 경우, "null" 이라는 내용으로 올것이며, 이럴 경우 기본 디렉토리의 내용을 돌려준다.

*리턴 타입 : List<FileItem> list - 현재 돌려줄 디렉토리의 내용들이 들어있는 리스트이다. 위의 FileItem 클래스를 사용하여 파일의 이름과 종류를 저장한다. 파일 타입의 경우, 디렉토리(isFile = false), 파일(isFile = true). 이는 json 형식으로 클라이언트에 전달된다.

*마지막에 println문으로 이런 요청이 왔었다는 간단한 메시지를 콘솔에 남긴다.


서버 끝


7-2. 안드로이드 구현


먼저 파일 목록을 표시해줄 UI를 작성한다.

RecyclerView를 사용하여 표시하도록 한다. 이를 위해 디팬던시에 RecyclerView를 추가한다. 방법은 저번 포스트에 있었던 OkHttp를 추가했던 것과 동일함.



UI를 만들어본다.


* 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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="streamer.sjh.com.dobbymusicstreamer.MainActivity">

<TextView
android:background="@color/colorBG"
android:textColor="@color/colorPrimaryDark"
android:paddingLeft="8dp"
android:gravity="center_vertical"
android:text="Music List"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="48dp" />

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

</LinearLayout>


대강 이런 느낌이 될 것이다. style에 정의해서 조금 더 소스를 간결하고 깔끔하게 바꿀 수도 있다.
또한 @color/colorBG 등의 요소들은 res/values/colors.xml을 간단하게 수정하면 추가할 수 있다.

MainActivity.java를 적절하게 수정해준다.
Context mainContext의 경우, 액티비티의 컨택스트가 핸들러 등에서 필요할 경우가 있으므로 미리 변수로 할당해둔다.
RecyclerView.setLayoutManager()를 하지 않은 상태에서 실행하면 에러가 나며 앱이 종료된다. 유의하자.

* MainActivity.java
public class MainActivity extends AppCompatActivity {

private Context mainContext;
private RecyclerView listView;

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

}

private void initFindViewById() {
listView = (RecyclerView) findViewById(R.id.listView);
listView.setLayoutManager(new LinearLayoutManager(mainContext));
}
}
이제부터 서버에서 받은 값을 ui에 활용해야 한다. 그러므로 저번에 구현한 HttpSender에 약간의 수정이 필요하다. 호출 시 Handler를 받아오도록 한다.


HttpSender.java

public abstract class HttpSender {

private static final String TAG = "HTTPSender";

private static final String URL = "http://ip:8080/";

protected String apiName;
protected RequestBody body;
protected Handler handler;

public HttpSender(Handler handler) {
this.handler = handler;
}

//Abstract Method
public abstract void setBodyContents(Object ... Params);

public void send() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
Request request = new Request.Builder().url(URL + apiName).post(body).build();
Message msg = handler.obtainMessage();
try {
Response response = client.newCall(request).execute();

msg.what = 0;
msg.obj = response.body().string();


} catch (IOException e) {
Log.e(TAG, "Exception occurred during HTTPSender send Method, Async Task.\nServer may not be reachable...");
msg.what = -1;
}
msg.sendToTarget();
return null;
}
}.execute();
}
}

변경, 추가된 부분은 붉은색으로 하일라이팅했음.


이제 이를 상속 받는 클래스를 만들어서 파일 목록을 받아오도록 한다.


* GetListSender.java

public class GetListSender extends HttpSender{


public GetListSender(Handler handler) {
super(handler);
apiName = "listfile";
}

@Override
public void setBodyContents(Object... Params) {
if (Params.length != 0) {
body = new FormEncodingBuilder().add("name", (String) Params[0]).build();
} else {
body = new FormEncodingBuilder().add("name", "null").build();
}
}
}

서버 부분에서 언급했듯이, 하위 디렉토리의 경우 그 디렉토리의 이름을 받아서 처리하고, 기본 디렉토리의 경우 "null" 을 받고 기본 디렉토리의 정보를 받아서 돌려준다.

HttpSender의 Send() 메소드에서 리스폰스로 받은 값을 넘겨줄 핸들러를 구현한다.

* ResultHandler.java
public class ResultHandler extends Handler {

private Context context;

public ResultHandler(Context context) {
this.context = context;
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0:
//request, response successful
String result = (String) msg.obj;
List<FileItem> list = dissolveJSONArray(result);
break;
case -1:
//request, response failure

break;
}
}}

HttpSender.Send()에서 정했듯이, 익셉션이 발생할 경우 msg.what = -1, 그렇지 않을 경우 0 이다.
익셉션이 발생했을 때의 대처는 차차 생각하도록 하고 우선 리스폰스가 재대로 왔을 때의 처리를 생각한다.
dissolveJsonArray(result)는 아직 구현하지 않았다.

서버의 리턴 타입이 List<FileItem>의 리스트 형식이므로, 실질적으로 리턴은 이렇게 될 것이다.

{ { fileName : a.mp3, isFile : true } , { fileName : b.mp3, isFile : true } , { filename : c, isFile : false }, ....... }

이런 Json구조의 문자열을 다시금 List<FileItem>형태로 바꿔주기 위한 작업이 필요하다.
우선 FileItem 클래스를 안드로이드에서도 구현한다.

* FileItem.java
public class FileItem {
public String fileName;
public boolean isFile;
public boolean isGoBack;

public FileItem(String fileName, boolean isFile) {
this.fileName = fileName;
this.isFile = isFile;
this.isGoBack = false;
}

public FileItem(String fileName, boolean isFile, boolean isTop) {
this.fileName = fileName;
this.isFile = isFile;
this.isGoBack = isTop;
}
}

서버와의 차이는 isGoBack이라는 boolean 변수의 추가이다.

만약 최상위 디렉토리가 아닐 경우, 뒤로 돌아가기 위한 버튼을 추가할 필요가 있기 때문에 추가하였다.

일반 파일 혹은 디렉토리의 경우 첫번째 생성자만을 써서 생성될 것이고, 만약 최상위 디렉토리가 아닐 경우에만 두번째 생성자를 쓴 객체가 하나씩 생성될 것이다.


다음으로, ResultHandler.java에 아래와 같은 메소드를 추가해준다.

private List<FileItem> dissolveJSONArray(String data){

List<FileItem> list = new ArrayList<>();
try {

if(StaticVals.MovedDirectory.size()!=0){
list.add(new FileItem("Go Back..", true, true));
}

JSONArray jsonArray = new JSONArray(data);
for(int i = 0 ; i<jsonArray.length(); i++){

JSONObject jsonObject = jsonArray.getJSONObject(i);
list.add(new FileItem(jsonObject.getString("fileName"), jsonObject.getBoolean("file")));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}


{ { fileName : a.mp3, isFile : true } , { fileName : b.mp3, isFile : true } , { filename : c, isFile : false }, ....... }

위와 같은 data를 파라미터로 받아서 List<FileItem> 형식으로 만들어준다.

만약 최상위 디렉토리가 아니라면, isGoBack값이 true이고, 이름 값이 "Go Back .."인 FileItem 객체가 생성될 것이다.


이러면 서버가 보내온 List<FileItem>을 활용할 준비가 되었다. 이를 RecyclerView에 표시해보자.

먼저 RecyclerView의 한칸 한칸에 해당하는 ui를 정의해본다.


*item_mainlistview.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:backgroundTint="@color/colorPrimary"
android:layout_height="48dp">

<ImageView
android:id="@+id/iv_fileType"
android:layout_marginLeft="8dp"
android:src="@drawable/directory"
android:layout_width="24dp"
android:layout_height="match_parent" />

<TextView
android:textSize="16sp"
android:text="test"
android:gravity="center_vertical"
android:layout_marginLeft="8dp"
android:textColor="@color/colorBlack"
android:id="@+id/tv_fileName"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>


위의 소스대로라면 아래와 같은 ui가 생성될 것이다.


다음 해야할 것은 RecyclerView에 List<FileItem>의 객체들을 표시해주기 위한 Adapter와 ViewHolder의 정의다.

Adapter<ViewHolder>를 상속받는 클래스와, ViewHolder를 상속받는 클래스를 각각 만들어준다.

ViewHolder를 상속받는 클래스를 Adapter<ViewHolder>의 이너클래스로 정의하였다.


* MainListAdapter.java

public class MainListAdapter extends RecyclerView.Adapter<MainListAdapter.MainListViewHolder>{

private List<FileItem> data = Collections.emptyList();
private Context context;

public MainListAdapter(List<FileItem> data, Context context) {
this.data = data;
this.context = context;
}

@Override
public MainListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_mainlistview, parent, false);
return new MainListViewHolder(itemView);
}

@Override
public void onBindViewHolder(MainListViewHolder holder, int position) {
holder.tv_fileName.setText(data.get(position).fileName);
if(!data.get(position).isFile){
holder.iv_fileType.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.directory));
}else{
if(!data.get(position).isGoBack)
holder.iv_fileType.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.music));
else
holder.iv_fileType.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.scrollupicon));
}
}

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

public class MainListViewHolder extends RecyclerView.ViewHolder{

TextView tv_fileName;
ImageView iv_fileType;
public MainListViewHolder(View itemView) {
super(itemView);
tv_fileName = (TextView) itemView.findViewById(R.id.tv_fileName);
iv_fileType = (ImageView) itemView.findViewById(R.id.iv_fileType);
}
}
}

onBindViewHolder에서 파일의 이름을 세팅하고, 그리고 각 타입(디렉토리, 음악, goBack) 별로 다른 아이콘을 쓰도록 하였다.

Enum을 썼으면 더 편리했을 것이라 생각하지만, 그냥 넘어가도록 한다. 

*public int getItemCount()에서 반드시 return값을 data.size()로 바꿔주도록 한다. 만약 기본값인 0으로 둔다면 리스트에 아무것도 뜨지 않는다.


이제 대강 준비가 마쳐졌다.

앱이 실행되자마자 서버에 리퀘스트를 보낼 수 있도록 MainActivity.java를 수정하고, 얻어진 List<FileItem> 객체를 RecyclerView에 반영하기 위해 핸들러를 수정한다.


* MainActivity.java

public class MainActivity extends AppCompatActivity {

private Context mainContext;
private RecyclerView listView;

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

//Get Directory info from server
HttpSender sender = new GetListSender(new ResultHandler(mainContext));
sender.setBodyContents();
sender.send();

}

private void initFindViewById() {
listView = (RecyclerView) findViewById(R.id.listView);
listView.setLayoutManager(new LinearLayoutManager(mainContext));
}

//Setup The RecyclerView with Actual data from the server.
public void initMainUI(List<FileItem> data) {
RecyclerView.Adapter adapter = new MainListAdapter(data, mainContext);
listView.setAdapter(adapter);
}
}

추가/변경된 소스는 붉은색으로 칠했다.

첫번째 부분에서는 ResultHandler 객체를 생성하면서 GetListSender객체를 생성하고, request를 보낸다.

두번째 변경된 부분(추가된 메소드)은 ResultHandler에서 호출될 것이다.

이를 위해 ResultHandler를 조금 수정한다.


public class ResultHandler extends Handler {

private Context context;

public ResultHandler(Context context) {
this.context = context;
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0:
//request, response successful
String result = (String) msg.obj;
((MainActivity)context).initMainUI(dissolveJSONArray(result));
break;
case -1:
//request, response failure

break;
}
}

private List<FileItem> dissolveJSONArray(String data){

List<FileItem> list = new ArrayList<>();
try {

if(StaticVals.MovedDirectory.size()!=0){
list.add(new FileItem("Go Back..", true, true));
}

JSONArray jsonArray = new JSONArray(data);
for(int i = 0 ; i<jsonArray.length(); i++){

JSONObject jsonObject = jsonArray.getJSONObject(i);
list.add(new FileItem(jsonObject.getString("fileName"), jsonObject.getBoolean("file")));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
}


이제 테스트를 해본다.


8. 테스트

사실 하위 디렉토리로 이동하는 것 까지 다룰려고 하였으나 너무 길어져서 그건 다음 포스트에...

이번 테스트는 서버의 기본 음악 디렉토리를 받아와서 안드로이드의 RecyclerView에 뿌리는것 까지만 하도록 한다.


안드로이드 앱이 켜지자마자 서버에 리퀘스트를 보낼 것이다.


 서버에서 리퀘스트가 잘 처리되었음을 알 수 있다.



안드로이드는 이런 느낌일 것이다.


9. +@ RecyclerView에 구분선 넣기

위의 안드로이드 화면을 보면 구분선이 없어서 뭔가 허전한 느낌이 있다. 간단하게 구분선을 추가해본다.

먼저 res/drawable에 다음 소스를 추가한다.


*line_divider.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<size
android:width="1dp"
android:height="1dp" />

<solid android:color="@color/colorPrimary" />

</shape>


그 후, 아래의 클래스를 추가한다.

* SimpleDividerItemDecoration.java

public class SimpleDividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;

public SimpleDividerItemDecoration(Context context) {
mDivider = context.getResources().getDrawable(R.drawable.line_divider);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();

int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);

RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();

int top = child.getBottom() + params.bottomMargin;
int bottom = top + mDivider.getIntrinsicHeight();

mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}


마지막으로, MainActivity의 initFindViewById()메소드에 아래와 같이 한줄을 추가한다.


private void initFindViewById() {
listView = (RecyclerView) findViewById(R.id.listView);
listView.setLayoutManager(new LinearLayoutManager(mainContext));
listView.addItemDecoration(new SimpleDividerItemDecoration(mainContext));
}

결과



구분선이 생겼다.


비교

(구분선 x / 구분선 o)




10. ToDo...

- 하위 디렉토리들로 움직이고 다시 올라올 수 있도록 한다.


 



 

 



Comments