회사에서 개발하고 있는 어플리케이션에 mvvm 아키텍처를 적용하면서 여러 시행착오를 겪고있는데, 이번에 해결한 문제가 가장 어이없고 짜증났던 것 같다.


기존의 백그라운드 서비스와 액티비티 간의 통신 방식을 브로드캐스트에서 event bus로 바꾸기 위해 코드를 수정하였다. 필요없는 코드들을 다 지우고 빌드를 해봤더니 error: cannot find symbol method setModel(BleServiceViewModel) 이 에러가 발생하는거다... 그래서 다른 부분에 문제가 있겠거니 해서 다른거 하고 다시 빌드했더니 같은 오류... 


(에러가 발생하는 코드)

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mFeatureFragBinding = FeatureFragBinding.inflate(inflater, container, false);

mBleServiceViewModel = FeatureActivity.obtainBleServiceViewModel(getActivity());
mFeatureFragBinding.setViewModel(mBleServiceViewModel);

return mFeatureFragBinding.getRoot();
}


도저히 뭐가 문제인지 몰라 구글링도 해보고 같은 프로그램의 정상 작동하는 다른 view-viewmodel 코드와 비교해도 오류가 발생할 껀덕지를 못 찾겠더라... 그렇게 몇 일을 날리다가 xml에서 class 이름을 커스터마이징했더니 깔끔하게 동작한다. 에러에 대한 힌트도 없고, 자료도 없고, 심지어 내가 뭘 잘못한 것도 아니라 해결을 하고도 굉장히 허무한 것 같다.


(해결)

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto">

<data class="FeaturesFragBinding">
<variable
name="viewmodel"
type="com.example.viewmodel.BleServiceViewModel" />
</data>
...
</layout>


정리

1. 코드를 수정하다보니 어느 순간 error: cannot find symbol method setModel(BleServiceViewModel)라는 오류 발생

2. Clean Project, Invalidate Caches & Restart, 프로그램 재시작 등을 해도 에러 발생

3. 코드에는 문제가 없음

4. generated databinding 클래스 이름 커스터마이징해주니 해결


가장 찝찝한건 원인을 알 수 없다는거다... 아마 라이브러리 자체의 문제 같은데

블로그 이미지

NCookie

,

동적으로 생성한 view들을 조작하고 싶은데, 해당 view들에는 id가 할당되어 있지 않아 난감한 적이 있었다. 그 때 찾은 방법이 있는데 자세한 코드와 내용은 링크를 참고하자.


LinearLayout ll = 
final int childCount = ll.getChildCount();
for (int i = 0; i < childCount; i++) {
      View v = ll.getChildAt(i);
      // Do something with v.
      // …
}



SparseArray<Edittext> array = new SparseArray<Edittext>();

private void findAllEdittexts(ViewGroup viewGroup) {

    int count = viewGroup.getChildCount();
    for (int i = 0; i < count; i++) {
        View view = viewGroup.getChildAt(i);
        if (view instanceof ViewGroup)
            findAllEdittexts((ViewGroup) view);
        else if (view instanceof Edittext) {
            Edittext edittext = (Edittext) view;
            array.put(edittext.getId(), edittext);
        }
    }

}



참고


android - Get all child views inside LinearLayout at once - Stack Overflow

블로그 이미지

NCookie

,

Activity와 Fragment의 설계 차이점이 궁금합니다



사실 안드로이드를 개발하면서 이해가 제일 되지 않던게 fragment였다. 액티비티랑 비슷한 역할을 하는데 context 같은거를 부르려면 귀찮은 과정을 거쳐야했다. 그래서 이걸 왜 쓰는지 한 번 알아보았는데 위 링크에서 잘 정리해준 것 같다.


그리고 나 같은 경우는 fragment를 사용하면 항상 코드가 난잡해졌었는데, 안드로이드의 개발 패턴 등에 대해서 공부를 해봐야 할 것 같다. 자세한 내용은 위의 링크를 참고하자.

블로그 이미지

NCookie

,

회사에서 이 라이브러리를 사용하게 되어서 주요 함수들에 대한 주석들을 번역해보았다.(github 주소)




[BleManager.java]



shouldAutoConnect()

protected boolean shouldAutoConnect() {
return false;
}


원격 디바이스에 한번만 연결할지(false), 아니면 사용 가능하게 되면 즉시 자동으로 연결될 디바이스 white list에 주소를 추가할지(true)를 반환한다. 후자의 경우, 블루투스 어댑터가 enable 되면 white list에 있는 디바이스들을 주기적으로 스캔하고, advertising packet을 수신하게 되면 그 디바이스에 연결하려고 시도한다. 연결이 끊겼을 때, 시스템은 그 디바이스에 재연결을 하려고 시도할 것이다. 이 함수가 true를 반환하고 디바이스 연결이 끊기면 BleManagerCallbacks의 onDeviceDisconnected 대신 onLinklossOccur 콜백 함수가 호출된다.


* 이 기능은 최신 기종의 안드로이드 디바이스에서 더 잘 작동하며, 오래된 디바이스는 동작하지 않을 가능성이 높다.

* 이 메서드는 bonded 디바이스만 사용되어야 한다. 그렇지 않은 디바이스들은 주소가 바뀔 수 있다. It will however work also with non-bonded devices with private static address. A connection attempt to a device with private resolvable address will fail(이 부분은 어떤 내용인지 잘 모르곘음)

* 디바이스의 첫 번째 연결은 항상 autoConnect 플래그가 false가 된다(BluetoothDevice의 connectGatt 함수 참고). 대부분의 사용자가 빠른 응답을 기대하고 있기 때문에 이러한 방법을 사용한다. 그러나 처음 연결하는 동안 이 메서드가 true로 반환되고 링크가 손실된 경우 manager 객체는 autoConnect를 true로 강제하는 BluetoothGatt의 connect 함수를 사용하여 reconnect 하도록 시도할 것이다. 



블로그 이미지

NCookie

,

어떤 프로그램이든 릴리즈를 할 때에는 개발 때 사용했던 로그가 찍히지 않도록 해야한다. 안드로이드에서는 기본적으로 Logger를 제공해주는데, 디버깅을 할 때에는 로그가 보여지도록 아래와 같이 커스텀 클래스를 만들 수 있다.


import android.util.Log;

public class Logger {
private static final String LOG_TAG = "APP_NAME";
private static final String FORMAT = "[%s]: # %s";

public static void v(String msg) {
if (!BuildConfig.DEBUG) return;
Log.v(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void d(String msg) {
if (!BuildConfig.DEBUG) return;
Log.d(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void i(String msg) {
if (!BuildConfig.DEBUG) return;
Log.i(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void w(String msg) {
if (!BuildConfig.DEBUG) return;
Log.d(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void e(String msg) {
if (!BuildConfig.DEBUG) return;
Log.e(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

private static String getCallerInfo() {
StackTraceElement[] elements = new Exception().getStackTrace();
String className = elements[2].getClassName();
return className.substring(className.lastIndexOf(".") + 1, className.length()) + "_" + elements[2].getLineNumber();
}
}


기존에는 Log에 사용할 TAG를 CLASSNAME.class.getSimpleName() 메서드를 사용했었는데, 클래스 이름을 아래의 두 가지 방법을 통하여 얻을 수 있다. 자세한 내용은 첨부된 첫 번째 링크를 참고하자.


  • Thread.currentThread().getStackTrace()
  • new Exception().getStackTrace();


어플리케이션이 디버깅 중인지, 릴리즈 상태인지를 알기 위해서는 다양한 방법이 있는데, 여기서는 BuildConfig를 사용하였다.



참고


[안드로이드] 커스텀 Logger 클래스 만들기


Log | Android Developer


stack overflow - 

Detect if I am in release or debug mode in android


블로그 이미지

NCookie

,

안드로이드에서 Java 8 기능을 사용하려고 만지작거리다가 한참 있다가 빌드를 했었는데 위와 같은 오류가 발생했었다. 나 같은 경우 이 오류를 아래의 코드를 삽입해서 해결했었다.


android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

구체적인 원인은 다시 알아봐야할 것 같다. 


이거 했다가 저거 했다가하니까 원인이 뭔지도 모르겠고 왜 해결됐는지도 모르겠다. 다음부터는 체계적으로 접근해야겠다.



참고


Invoke-customs are only supported starting with android 0 --min-api 26



블로그 이미지

NCookie

,

회사에서 앱을 개발하는데 가장 최근에 열린 activity를 알아오는 기능이 필요했었다. 그래서 찾아보다가 리플렉션도 잠깐 건드려보기도 했는데, 굉장히 간단한 방법을 찾았다.


어려운 개념도 아니다. Application.ActivityLifecycleCallbacks라는 인터페이스를 구현해주기만 하면 된다. 메소드로 onActivityStarted, onActivityResumed 등이 있는데, 매개변수로 onStart, onResume 등을 호출한 Activity가 전달된다. 이걸 manifest에서 등록한 application 파일에서 구현하면 쉽게 최근 activity을 얻어올 수 있다.


public class WWAApplication extends Application { public static Activity mCurrentActivity = null; ActivityLifecycleCallbacks mActivityLifecycleCallbacks = new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { mCurrentActivity = activity; Log.i("WWAAplication", activity.getClass().getSimpleName()); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } }; @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks); } }


callback register 해주는 것 잊지 말고.


나는 이것을 이용하여 사용자가 어플리케이션 화면을 벗어나거나 화면을 off 하면 notification bar를 띄우고, 다시 화면으로 돌아오면 notification을 disable하였다. 이 부분에 관해 자세한 방법은 No, You Can Not Override the Home Button... But You Don't Have To! 참고.

블로그 이미지

NCookie

,

현재 회사에서 진행하고 있는 프로젝트에서 안드로이드 어플리케이션은 특정 디바이스와 계속해서 블루투스 연결을 유지해야 한다. 그러다보니 Service와 Broadcast 같은 것들에 대해 알아보게 되었는데, Service 부분이 헷갈리는게 있어서 정리하려고 한다.



안드로이드의 서비스는 life cycle에 따라 종류가 두 가지로 나누어진다.




1. Started Service


startService 메소드를 사용하면 서비스가 생성되며, stopService 또는 stopSelf를 호출하지 않는 이상 서비스는 종료되지 않고 계속 작동한다.


이러한 형태는 백그라운드에서 계속 돌아가야 하는 음악 재생 어플리케이션 등에서 사용된다.


그리고 서비스의 life cycle에 따라 콜백 메소드들이 존재한다.


하나의 서비스가 생성될 때 onCreate가 단 한번만 호출된다.


예전에는 onStart를 사용하였지만 요즘은 onStartCommand 사용을 권장하고 있다. 서비스가 시작될 때 호출되는데, 만약 이미 startService를 했는데 추가로 startService를 하려고 하면 이 onStartCommand가 호출된다. bindService를 사용할 때는 호출되지 않는다.


그리고 이 메소드는 int형의 반환값을 가지는데, 리소스 부족 등의 이유로 시스템이 서비스를 강제종료 했을 떄, 어떻게 처리할지를 정하는 플래그이다. 


- START_STICKY : client에서 startService 사용 시 intent로 서비스를 전달하며, onStartCommand에서 intent를 인자로 받을 수 있다. 단, 기존의 전달되었던 intent 값은 넘어오지 않는다.


- START_NOT_STICKY : 시스템이 서비스를 죽여도 다시 살아나지 않는다.


- START_REDELIVER_INTENT : 기본적으로 START_STICKY 플래그와 비슷하며, 대신 기존에 전달했던 intent 값을 돌려준다.


서비스 생성 : startService(Intent service)

서비스 종료 : stopService() 또는 stopSelf()



2. Bound Service


bindService 메소드를 사용하면 서비스가 생성되며, unbindService를 사용하거나 기존에 bound 된 액티비티들이 모두 unbind 하면 종료된다.


명시적으로 메소드를 사용해야 종료되는 Started Service와는 달리, 생성한 서비스에 bound 된 액티비티가 존재하지 않는다면 서비스는 종료된다.


콜백 메소드로는 onBind, onRebind, onUnbind가 있다. onRebind 같은 경우, 이미 unbind 되었던 액티비티가 다시 서비스에 연결했을 때 호출된다. 내 프로젝트를 예로 들자면 사용자가 앱 화면을 벗어나 다른 작업을 할 때 unbind가 되는데, 현재 디바이스가 연결되어 있다는 것을 notification bar로 보여준다. 그리고 다시 화면으로 돌아와 rebind 되면 notification을 내린다.


Bound Service 또한 마찬가지로 서비스에 플래그를 전달할 수 있는데, bindService 메소드의 인자로 값을 설정한다. 흔히 사용하는 플래그로는 0과 BIND_AUTO_CREATE가 있다.


0을 플래그로 설정하면 startService로 서비스를 생성하기 이전까지는 서비스가 생성되지 않는다. 만약 기존에 서비스가 생성되어 있다면 정상적으로 bind 된다.


BIND_AUTO_CREATE는 시스템에 의해 강제로 종료되더라도 리소스가 충분해지면 리소스에서 다시 실행하도록 하며, onBind에서 이전에 전달받았던 Intent 값을 받을 수 있다. 즉 바인딩된 액티비티가 존재하는 한 서비스를 생성한다. 대신 onStartCommand는 startService를 사용할 때에만 호출된다.


서비스 생성 : bindService(Intent service, ServiceConnection conn, int flags)

서비스 종료 : unbindService() 또는 모든 액티비티 unbound



3. start와 bind 동시 사용


startService와 bindService를 동시에 사용하는 것 또한 가능하다. 


이 때 서비스의 life cycle은 Started Service를 따른다. 즉, 명시적으로 stopService 또는 stopSelf 메소드를 사용하지 않는 이상 서비스는 종료되지 않는다.(시스템이 종료하는 것은 제외하고...)


이에 대해서는 구글에 좋은 예제가 있다.



참고


[Android] 서비스(Service)의 기초


19. Service에 대해서 - Service의 생존(2)


[컴][안드로이드] service에 bind하기 - local service


android service startService() and bindService()


Use 0 or BIND_AUTO_CREATE for bindService's flag


Google Android developers Context#BIND_AUTO_CREATE

블로그 이미지

NCookie

,

Serializable 을 implements 해서 BluetoothDevice 객체를 Intent에 넣어서 전달하려고 했는데, 막상 받아보니 null 값이 들어있었다.


그래서 찾아보니, BluetoothDevice 클래스 선언 단계에서 Parcelable 을 implements 하였고, 필요한 함수들도 이미 구현되어 있다고 한다.


그래서 Parcelabe을 implements 하거나 그에 따라 필요한 함수들도 따로 구현할 필요가 없다.


그냥 값을 넘길 때


intent.putExtra(intent_key, mSelectedDevice);
startActivity(intent);

하고, 값을 받으려면


Intent intent = getIntent();
BluetoothDevice bluetoothDevice = intent.getExtras().getParcelable(intent_key);


이렇게 하면 된다. 엄청 편하다.



참고


Passing BluetoothDevice Object to Another Activity though Intent


Google Developers : BluetoothDevice

블로그 이미지

NCookie

,

png 이미지를 안드로이드에서 사용했는데, 위 아래의 여백이 있었다.


그러나 실제 이미지에는 그런 여백이 없었다.


이런 현상은 android:adjustViewBounds 속성을 이용하여 해결할 수 있다. 이 속성은 drawable의 원래 비율을 유지하기 위해 뷰의 영역을 조절할지 정한다.


그렇기 때문에 이 속성을 true로 하면 여백을 없앨 수 있다.


만약 안드로이드의 API 가 17 이하라면, 이미지가 이미지뷰보다 클 때에만 적용된다. 자세한 내용은 아래의 링크를 참고하자.




참고


Google Developers ImageView


[안드로이드] SDK 17 이하에서 ImageView의 adjustViewBounds 속성 문제

블로그 이미지

NCookie

,