반응형

오랜만에 안드로이드 스튜디오 프로젝트를 열고 Gradle Sync 도중 위와 같은 오류가 발생하였다.

무언가 해당 Gradle 버전에 종속적인 문제가 발생한 것으로 예측된다.

 

이 때 간단히 해결할 수 있는 방법으로는 정상적으로 빌드될 수 있는 안정된 Gradle 버전을 찾는 것이다.

 

우선 내가 사용하고 있는 버전을 확인해보자.

안드로이드 스튜디오에서 File -> Project Structure -> Project 탭을 보면 아래와 같은 창을 확인할 수 있다.

내가 사용하는 플러그인 버전은 3.2.1이고, Gradle 버전은 5.1.1이다.

이 버전을 변경하기 위해서는 플러그인 버전에 따라 필요한 Gradle 버전이 무엇인지를 알아야 한다.

플러그인과 Gradle 버전 관계표

나는 기존 플러그인 버전이 3.4.1이었기 때문에 Gradle 버전을 5.1.1 버전을 사용했었다.

여기서 플러그인 버전을 3.2.1로 설정하고, Gradle 버전을 4.6으로 변경하여 사용할 것이다.

 

프로젝트 최상단 폴더의 build.gradle 을 열면 플러그인 버전을 변경할 수 있다.

위와 같이 기존 플러그인 버전 대신 3.2.1로 변경하고, 그 다음에는 Gradle 버전을 변경해주어야 한다.

프로젝트 폴더\gradle\wrapper\gradle-wrapper.properties 파일을 열면 버전을 수정할 수 있다.

위와 같이 기존 버전 대신 4.6 버전을 넣어주었다.

 

이제 위의 Try Again 버튼을 클릭하여 문제가 해결되었는지 확인하면 된다.

정상적으로 빌드까지 마친 것을 확인하였다.

혹시나 위의 버전으로도 오류가 난다면 위 '플러그인과 Gradle 버전 관계표'를 보고 적절한 버전을 찾아가야 할 수도 있다.

반응형
반응형

 

플러터는 크로스플랫폼 개발 툴이다.

여기서 크로스플랫폼 개발이라 하면 Android/IOS 모바일 개발을 동시에 할 수 있는 것을 말한다.

 

나는 안드로이드 개발만 하다가 IOS 개발까지 해야함에 따라 크로스플랫폼 개발을 고려하게 되었다.

처음 크로스플랫폼을 접할 때 크로스플랫폼이 가질 수 있는 한계에 대해 매우 고심했고,

다양한 크로스플랫폼을 최대한 비교하여 가장 효율적이고 개발에 가장 유리할 수 있는 크로스플랫폼을 찾고자 했다.

 

크로스플랫폼을 쓰고자 하는 목적은 보통 생산성에 맞추어져 있다.

구현하고자 하는 기술에 문제가 되지 않는다면 크로스플랫폼 개발은 매우 효율적이고 올바른 선택이 될 것이다. 네이티브로 개발한다면 안드로이드와 IOS 플랫폼을 각각 개발하여 두배의 시간 혹은 두배의 인력이 드는데 크로스플랫폼을 쓴다면 비용적으로나 시간적으로나 매우 유리해지기 때문이다.

 

나는 크로스플랫폼 개발 툴 중 자마린, 리액트 네이티브, 플러터 셋중에 무엇을 사용할지 고민을 하였다.

자마린의 경우 나는 C#도 주로 사용했던 언어였기 때문에 그 접근성 때문에 고민했었고,

리액트 네이티브는 자바스크립트 기반인 부분에 이끌렸다. 자마린과 양대 산맥이기도 하고...

 

플러터의 경우는 자마린, 리액트네이티브 사이에서 혜성처럼 갑자기 떠오른 느낌이었고,

다트라는 조금 낯선 언어(그래봤자 자바와 비슷한 느낌)를 사용하지만 구글에서 밀고 있기도 하고, 여러 자료를 찾아본 결과 UI 구현에 대해 상당한 강점이 있으리라 판단했다. 


서론이 길었는데 내가 내린 판단은 플러터를 사용했을 때 가장 생산성 높은 개발을 이룰 수 있다라는 것이다.

우선, UI 구현 자체가 매우 수월하다. 초보자들도 복잡한 UI를 플러그인만 잘 갖다사용하여도 어렵지 않게 구현해낼 수 있다. UI가 별거 아닌 작업처럼 보이지만, 다이나믹한 UI를 구현하기 위해서는 상당한 시간을 할애해야 한다.

 

다른 개발툴을 플러터만큼 파보지 않아서 판단하기에 오류가 있을 수 있지만, 

UI 생산성은 자마린, 리액트 네이티브도 따라오기 어려울 것 같다는 판단이 든다.

 

사실 이 UI 구현에 대한 장점만으로도 플러터를 사용할 이유는 충분했다.

네이티브 개발시 레이아웃 구성이나, 다이나믹한 UI를 구현할 때마다 꽤 시간을 들여야 했던 것을 떠올려보면,

플러터는 그 시간을 날로 먹는(?) 수준으로 줄여준다. 거의 로직에만 신경 쓸 수 있도록 말이다.

 

한가지 더 기대하고 있는 것은 기획 디자인 툴인 Adobe XD에서 'XD To Flutter' 기능을 출시할 예정에 있다.

(얼마 전 출시는 된 상태인 것 같은데 어느 정도 수준인지 확인해보지는 못했다.)

현재 제플린에서 Flutter Extension을 제공하고 있지만, 경험자에게 듣기로는 수동으로 작업하는게 더 빠를 정도로 코드 변환이 매우 지저분하다고 한다. 그래서 XD To Flutter 기능이 완벽하게 출시된다면 UI 작업은 개발자가 거의 신경을 쓰지 않아도 될 수준이 되지 않을까 싶다. 디자이너가 넘겨준 XD파일을 그대로 변환시키면 될 것이므로..

 

하지만, 역시나 단점도 존재한다. 내가 꼽은 단점은 아직 플러터가 리액트네이티브나 다른 크로스플랫폼에 비해 활성도가 떨어진다는 점이다. 자료가 타 개발 도구에 비해 부족하다는 것. 예를 들어, 원하는 기능을 구현할 때 해당 기능에 대한 플러그인이 존재하면 가져다쓰면 금방 개발 가능하지만, 없다면 그에 해당하는 플러그인을 직접 만들어야 한다. 

혹은 존재하더라도 베타 버전의 플러그인이라 다른 플러그인과 충돌하여 컴파일에 오류가 발생하거나 하는 등의 문제도 존재한다. 그래도 이 부분은 점점 개선이 될 것으로 생각하고 있다. 내가 플러터를 접한지 1년이 안되었는데 그 사이에도 문제가 있었던(혹은 베타였던) 플러그인이 많이 개선되었다.

 

개발에 대한 안정성만 놓고 보면 아직까지는 리액트 네이티브가 한 수위라고 생각되지만,

UI 개발 생산성에 대한 매력이 너무 크기 때문에, 그 외 자료 부족 등의 불편한 부분은 감안할 정도라고 본다.

플러터로 간단한 앱부터, 플랫폼 수준의 앱까지 개발해보았는데, 앞으로도 플러터를 애용할 것 같다.

 

반응형
반응형

 

안드로이드에서 터치 기능에 대해 구현할 때 onTouchEvent() 함수를 Override하여 사용하게 된다.

 

일반적으로 동시 터치가 아닌 한 포인트의 터치만 구현할 경우 아래와 같이 사용을 한다.

 

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                    global_x = (int) (event.getX());
                    global_y = (int) (event.getY());
                    break;

            case MotionEvent.ACTION_MOVE:
                    global_x = (int) (event.getX());
                    global_y = (int) (event.getY());
                    break;
        }

        return true;
    }

 

 

간단히 설명을 하면 발생한 이벤트를 마스킹하여 현재 어떤 모션 이벤트(ACTION_DOWN? ACTION_MOVE)가 발생했는지 확인하여 그에 적절한 처리를 하게 된다.

 

위의 코드는 한 포인트에 대해서만 처리될 뿐 두 개 이상의 포인트에 대해서는 고려되있지 않다.

 

아래 코드는 두 개의 포인트에 대해 터치를 구현한 코드이다.

 

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int pointer_count = event.getPointerCount(); //현재 터치 발생한 포인트 수를 얻는다.
        if(pointer_count > 2) pointer_count = 2; //3개 이상의 포인트를 터치했더라도 2개까지만 처리를 한다.

        switch(event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: //한 개 포인트에 대한 DOWN을 얻을 때.
                    global_num[0] = event.getPointerId(0); //터치한 순간부터 부여되는 포인트 고유번호.
                    global_x[0] = (int) (event.getX());
                    global_y[0] = (int) (event.getY());
                }
                break;

            case MotionEvent.ACTION_POINTER_DOWN: //두 개 이상의 포인트에 대한 DOWN을 얻을 때.
                for(int i = 0; i < pointer_count; i++) {
                    global_num[i] = event.getPointerId(i); //터치한 순간부터 부여되는 포인트 고유번호.
                    global_x[i] = (int) (event.getX(i));
                    global_y[i] = (int) (event.getY(i));
                }
                break;

            case MotionEvent.ACTION_MOVE:
                for(int i = 0; i < pointer_count; i++) {
                    global_num[i] = event.getPointerId(i);
                    global_x[i] = (int) (event.getX(i));
                    global_y[i] = (int) (event.getY(i));
                }
                break;
        }

        return true;
    }

 

코드가 크게 길어지지도 않았고, 그리 어렵지도 않다.

 

3번째 코드에서 event.getPointerCount() 함수로 현재 터치된 포인트 수를 가져온다.

 

두 개의 손가락으로 터치하였다면 2를 반환할 것이며, 그 이상의 터치를 발생시켰을 경우 그에 대한 값을 반환할 것이다.

 

위의 코드에서는 두 개의 포인트에 대해서만 처리할 것이므로 4번째 줄에서 3개 이상의 포인트가 인식되었더라도 강제로 2개의 포인트만 처리하도록 하였다.

 

7번째줄과 15번째 줄을 보면 ACTION_DOWN, ACTION_POINTER_DOWN 이렇게 두개로 나눠져있다.

 

주석에도 설명되어있지만 ACTION_DOWN은 한 개에 대한 포인트에 대해 DOWN 이벤트가 발생했을 경우이고,

 

ACTION_POINTER_DOWN은 두 개 이상에 대한 포인트에 대해 DOWN 이벤트가 발생했을 경우이다.

 

이게 무슨 의미냐면, 두 개 이상의 터치가 발생했을 때는 ACTION_DOWN 이벤트는 발생하지 않는다.

 

반대로 한 개의 터치가 발생했을 때는 ACTION_POINTER_DOWN 이벤트가 발생하지 않는다.

 

즉, 구현하려는 어플이 두 개 이상의 포인트를 동시에 터치할 경우도 있고, 한 개의 포인트만을 터치할 경우도 있다면

 

위와 같이 두 개의 이벤트를 모두 구현해주어야 하는 것이다.

 

왜 이렇게 비효율적으로 구현했는지가 의문이지만 어쨌든 이렇게 구현을 해주어야 한다.

 

다행히도 ACTION_MOVE 이벤트의 경우는 한 개만 터치할 경우나 두 개 이상을 터치할 경우나 모두 같은 이벤트로 들어오게 되므로 별도로 나눠서 구현할 필요가 없다.

 

다음으로 8번째 줄을 보면 ACTION_DOWN의 경우는 당연히 한 포인트를 터치했을 때만 발생하므로 0번 배열에 해당 좌표 데이타를 보관하게 된다.

 

15번째 줄을 보면 발생한 포인터 수만큼 for문을 돌려 배열에 해당 좌표들을 저장하게 된다.

 

여기서 중요한 것이 16번째 줄을 보면 event.getPointerId(i)로 현재 발생한 포인터에 대한 고유 번호를 따로 저장해주고 있다.

 

이것이 왜 필요하냐면, 만약 2개 포인트를 터치하고 있다고 가정해보자.

 

global_num[0] = 0

global_x[0] = 50

global_y[0] = 50

 

global_num[1] = 1

global_x[1] = 180

global_y[1] = 180

 

위 상태에서 처음 터치한 손가락을 떼면 어떻게 될까?

 

global_num[0] = 1

global_x[0] = 180

global_y[0] = 180

 

global_num[1] = 0

global_x[1] = 0

global_y[1] = 0

 

1번 배열에 있던(두번째로 터치한 포인트) 좌표 데이타가 0번 배열로 넘어갔다.

 

당연히 손가락 하나를 뗐으므로 한 포인트만 터치되고 있는 상태로 바뀌었을 것이므로,

 

1번 배열의 데이타는 사라지고 현재 터치되고 있는 좌표 데이타가 0번 배열의 데이타로 넘어가게 된 것이다.

 

하지만 배열의 순서가 바뀌었더라도 터치 상황을 문제없이 인지할 수 있는 방법은 바로 고유 번호의 확인이다.

 

배열의 순서는 바뀌었지만, 고유 번호는 그대로 1을 유지한 채로 넘어가있다.

 

따라서, 다중 터치를 구현할 때 배열의 인덱스를 이용해서 처리하지 않고 고유 포인터 아이디 값을 확인하여 처리해야 문제되지 않는다.

 

위의 내용을 이해한다면 터치 포인트가 3개 이상인 것도 얼마든지 구현이 가능하다.

 

주의할 점을 다시 정리해보면 아래 두가지 정도인 듯 하다.

 

1. ACTION_DOWN은 터치 포인트가 1개일 때만 발생하고 ACTION_POINTER_DOWN은 터치 포인트가 2개 이상일 때만 발생하므로, 필요에 따라 두 가지를 모두 구현해주어야 한다.

 

2. 다중 터치 도중 하나 이상의 터치 포인트가 해제될 경우 좌표 변수 안에 입력되있는 좌표 데이타의 배열 순서가 뒤바뀔 수 있으므로 터치 포인터의 고유 번호를 비교하는 방식으로 구현한다.

 

반응형
반응형

 

SharedPreferences 객체는 제목 그대로 환경 설정 데이타를 간단히 저장할 수 있는 객체이다.

 

물론, 굳이 이것을 사용하지 않더라도 직접 파일 형태로 저장하여 관리할 수도 있다.

 

하지만 객체에서 지원해주는 만큼 간편하게 사용할 수 있기 때문에 간단한 내용을 저장할 때는 자주 사용된다.

 

나의 경우는 유틸 클래스를 별도로 만들어서 이 객체를 통해 환경설정 내용을 불러오거나 저장할 수 있도록 구현해놓고 사용한다.

 

package com.project.core;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;

 

public class Util {
    public static void ConfigSave(Context context, String Name, String Value){
        SharedPreferences pref = context.getSharedPreferences("Conf", 0);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString(Name, Value);
        editor.commit();
     }


     public static String ConfigLoad(Context context, String Name){
         SharedPreferences pref = context.getSharedPreferences("Conf", Activity.MODE_PRIVATE); 
         String data = pref.getString(Name, "");
         return data;
     }
}

나의 경우는 위와 같이 구현해놓고 사용한다.

 

[저장할 때]

ConfigSave(context, "TEST", "1"); //환경 변수 TEST에 1을 저장한다.

 

[불러올 때]

String test = ConfigLoad(context, "TEST"); //환경 변수 TEST의 내용을 불러온다.

 

추가로, SharedPreferences 객체의 내용을 모두 비우거나, 저장한 특정 환경변수의 내용을 제거할 수도 있다.

SharedPreferences의 remove() 함수를 사용하면 특정 환경변수의 내용을 제거할 수 있으며,

clear() 함수를 사용하면 모든 내용이 제거된다.

함수 사용 후에는 반드시 commit() 함수를 호출 시켜주어야 한다.

 

 

반응형
반응형

예를 들어서 안드로이드 클라이언트가 다른 외부 서버랑 연결해서 소켓 연결 후 통신을 주고 받는데,

 

아스키코드를 주고 받을 때는 문제가 없지만 한글을 주고 받을 때는 깨지는 현상이 생깁니다.

 

이것은 인코딩과 관련된 문제로 주고 받을 때 클라이언트쪽에서 송수신할 때에 아래와 같이 처리를 해주면 됩니다.

 

보통 Buffered 객체를 많이 쓰므로 이것으로 예를 들겠습니다.

 

//소켓 연결 처리.

SocketAddress remoteAddr=new InetSocketAddress("127.0.0.1",1234);

Socket socket=new Socket();

socket.connect(remoteAddr); //remoteAddr

 

//Buffered 객체 연동.

BufferedOutputStream out=new BufferedOutputStream(socket.getOutputStream()); //output stream

BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream(),"EUC_KR")); //input stream.

 

위 코드를 보면 out은 서버로 데이타를 내보낼 때 쓰고, in은 서버에서 데이타를 받을 때 씁니다.

Reader 객체 생성시 EUC_KR를 주게 되면 서버에서 받는 데이타는 한글이라도 깨지지 않습니다.

 

전송할 때는

 

String data="한글테스트\n";

out.write(data.getBytes("EUC_KR"));

 

이와 같이 EUC_KR 로 인코딩하여 전송하면 해결됩니다.

 

간단한 내용이지만 이것으로 헤매는 분들이 계시기에 올려둡니다...

 

반응형
반응형

 

처음에 안드로이드로 TCP 소켓 통신시 겪었던 문제입니다.(자바를 이용한 모든 TCP 소켓 통신에도 해당)

 

C++로 개발한 더미 클라이언트를 통해 서버 PC로 데이타를 무한히 날리면 일정한 속도로 잘 날아가는데,

 

안드로이드에서 해당 더미 클라이언트를 구현하여 데이타를 무한히 날리니 빨리 전송되다가 느리게 전송되다가

 

송신 속도가 일정치 않고 왔다갔다하는 현상이 나타났습니다.

 

확인 결과 NAGLE 알고림즘이 적용되있었기 때문입니다.

 

socket 클래스의 메소드 중에 setTCPNoDelay(boolean on) 함수가 존재하는데 이것을 true로 설정 후 송신하게 되면

 

버퍼링 없이 바로바로 데이타가 송신됩니다. 참고로 default는 false입니다.

 

※ NAGLE 알고리즘은 데이타를 효율적으로 송신하기 위한 알고리즘인데, 송신시에 일정량의 데이타를 버퍼에 모아놓았다가 한꺼번에 보내는 방식입니다. 게임 같이 실시간으로 패킷이 왔다갔다해야하는 경우는 적합하지 않겠죠.

반응형

+ Recent posts