Flutter 안드로이드 위젯 구현 / 특정 화면으로 routing하기
며칠을 삽질하며 완성했다.
그것도 이것보다 중요한게 있는 시기에 ㅜㅜ 이거 삽질에만 며칠을 쏟았다.
저처럼 고생하지 마시고, 도움이 되시길 바랍니다!!
플러터만으로도 대부분의 기능들을 편리하게 사용할 수 있지만, 안드로이드 시스템 API를 사용해야 한다거나, 성능상의 문제로 native 코드를 작성해야 하는 경우가 있다. pub.dev의 대부분의 패키지 뿐 아니라 native code 와 병행해서 만들어진 flutter package 들도 아주 많다.
플러터와 안드로이드 네이티브 간 통신(MethodChannel)에 대한 구현도 있을 것이고
플러터의 동작 원리로부터, 안드로이드 코드를 수정하여 플러터의 동작을 제어하는 구현도 있다.
그 중 이번에는, 네이티브와 연동한 홈 위젯 구현에 대해 정리할 것이다.
home_widget을 사용할 것이다.
https://pub.dev/packages/home_widget
이번 글의 설명 기준은 안드로이드에서의 구현이며, iOS에서도 구현하려면 추가적인 작업이 필요하다.
참고로 Flutter에서 안드로이드 작업을 해줄 때엔
무조건 android 파일 -> 오른쪽버튼 -> Flutter -> Open Android module in Android Studio 로 열어서 작업해줘야 한다.
그래야 문제없이 작업을 진행할 수 있다.
1. 위젯의 설정값
android/app/src/main/res/xml/example_appwidget_info.xml
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="72dp"
android:minHeight="72dp"
android:minResizeWidth="148dp"
android:minResizeHeight="148dp"
android:widgetCategory="home_screen" />
minWidth가 72이면 최대 1픽셀 잡아먹을 수 있는 크기이다.
나는 1x1로 위젯을 만들 것이라서 min을 72로 잡았다. 더 여러가지 크기는 여기서 확인할 수 있다.
2. 위젯의 모양
android/app/src/main/res/layout/widget_layout.xml
<FrameLayout
android:id="@+id/forever"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="72dp"
android:layout_height="72dp" >
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:src="@drawable/cam"
/>
</FrameLayout>
나는 카메라 위젯을 만들 것이고, 이걸 누르면 내 앱의 기능 중 '카메라' 기능으로 이동하게끔 할 것이라서 이렇게 작성했다.
3. 위젯 기능 구현
android/app/src/main/kotlin/AppWidgetProvider.kt
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetProvider
class AppWidgetProvider : HomeWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
val intent = Intent(context, CameraActivity::class.java);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
setOnClickPendingIntent(R.id.forever, pendingIntent)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
위젯을 클릭하면, CameraActivity로 이동하게끔 구현했다.
Android 12부터, PendingIntent를 실행할 때, FLAG_IMMUTABLE 또는 FLAG_MUTABLE 둘 중 하나를 설정해야만한다.
android/app/src/main/kotlin/CameraActivity.java
import java.util.ArrayList;
import java.util.List;
import io.flutter.embedding.android.FlutterActivity;
public class CameraActivity extends FlutterActivity {
@NonNull
@Override
public String getDartEntrypointFunctionName() {
return "main"; // main 대신 다른값 넣으면 특정 함수로 entry point 지정 가능
}
@Nullable
@Override
public List<String> getDartEntrypointArgs() {
List<String> li = new ArrayList<String>();
li.add("Cam");
return li;
}
}
위젯을 클릭했을 때, 내 앱의 특정 화면으로 이동시키고 싶을 때
크게 두 가지 방법이 있다.
1. getDartEntrypointFunctionName() 오버라이딩
=> 여기에 메인 엔트리 포인트 진입점으로 할 함수명을 넣어주고, 그 함수를 main.dart에 정의해주면 된다.
=> 기본값은 main이며, 이는 우리가 Flutter에서 void main() { runApp(....)} 하는 부분이다.
2. getDartEntrypointArgs() 오버라이딩
=> 기본적으로 우리가 Flutter의 메인함수를 보면 void main() {} 이러하다. 여기서 인자가 없음을 알 수 있다.
=> getDartEntrypointArgs() 를 오버라이딩하여 List<String> 을 넘겨주고, Flutter의 메인함수를 void main(List<String>? list) {} 로 수정하여, 어디서 어느 목적으로 main함수를 실행시켰는지를 구분해 줄 수 있다.
4. 컴포넌트 기능 상세
AndroidManifest.xml
<Application> </Application> 이 태그 사이에 아래 부분들을 넣어준다.
<activity android:name=".CameraActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
>
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<receiver android:name="AppWidgetProvider" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
Android 12부터 exported를 명시적으로 넣어줘야한다.
위젯 자체적으로 특정 기능을 수행하는게 목적이 아니었으므로, 이렇게 하면 구현이 완료된다.
Flutter의 main.dart의 메인함수의 인자를 void main(List<String>? list) {} 이렇게 만들어 주고, 인자에 따라 진입점을 구분해주어
이용해주면 된다!
위젯 자체적인 기능( 카운팅이나 기타 및 )을 추가하고 싶으면 home_widget API를 참고하면 된다!!
여기까지 구현하고 트러블슈팅하는데에 굉장히 오랜시간이 걸렸다..
부디 나처럼 고생하는 사람이 없기를 바라면서..
도움이 되셨으면 좋겠습니다!!
참고