두두두둥!!! 인생 첫 앱 게시!
아직 검토 중이지만 그래도 여기까지 왔다는 것에 뿌듯하다!!!
희미한 기억을 또렷한 기록으로 남기기 위해 끄적 하는 포스팅!
왜 모래시계 타이머인가?
위에 출시 버전명이 보이는 것처럼
"인생" 처음으로 게시하는 앱은 모래시계 타이머이다.
뜬금없지만, 나는 잡담의 힘을 좋아하는 편이다.
내가 생각하지 못했던 부분을 생각하게 되는 경우도 많기도 하고
"뭔가 해보고 싶은 걸?"라는 의욕을 만들어주는데
이번에 한 모래시계 타이머도 잡담의 힘 덕에 만들어진 앱이다.
정말 이야기하다가 나온 주제가 회의 시간에 이야기가 길어지는 경우가 있고, 그 문제를 해결하기 위해 스탠딩 회의나 모래시계 등을 활용하지 않냐 등의 대화였는데, 대화를 하다 보니 모래시계 타이머가 만들어보고 싶어 졌고, 어제 새벽(6일)부터 지금까지 재미있게 코딩했다.
그리고 내가 좋아하는 우리 가족, 지인분들 그리고 게더에서 알게된 좋은 분 덕에 더 재미있게 했다. 😆
자 그러면 모래시계는 어떻게 구현할 것 인가?
솔직히 말하면 모래시계를 구현하려면 어떻게 해야할지 막막했다.
- 모션으로 구현해야하는 걸까?
- 파워 포인트로 모래시계 로딩 화면을 만들 수 있던데
(https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=richkoh&logNo=220778899280) - 모래시계를 사서 촬영해볼까?
그러다 관련 pub package이 있지 않을까? 검색해보니!!
flutter_spinkit이 있었다!
심지어 모래시계뿐만 아니라 다양하게 많았다
그래서 선택한 위젯은 PouringHourGlassRefined (지금 보니 PouringHourGlass도 에뻐보임)
처음엔 랜덤으로 나오게 할까 생각도 했다가 타이머가 고정이 아니라서 const을 사용할 수 없기에 우선 보류하기로 했다
SpinKitPouringHourGlassRefined(
color: Theme.of(context).colorScheme.onPrimaryContainer,
duration: Duration(seconds: seconds),
size: MediaQuery.of(context).size.width * 0.5,
)
작업한 소스는 이게 끝!
아쉬운 건 딱 정사각형으로만 구현이 돼서 길쭉한 모래시계는 할 수 없었다
타이머 설정하기
자 이제 모래시계를 만들었으니 타이머 설정을 해야 한다
모래시계처럼 타이머 설정도 괜찮은 pub package이 있지 않을까! 했지만 마음에 드는 게 없었다 😂
Material design Time pickers
그래서 Material design Time pickers을 해봤다
다 좋다 키보드 입력으로도 할 수 있고, 익숙한 UI인 시계 형식으로 지정할 수 있고 기능적으로 많다
하지만 쓰지 않은 이유는 타이머 설정을 빨리 하고 싶은데 시, 분 따로 선택해야 한다는 점이 가장 컸다.
그렇다고 키보드 입력도 귀찮을 거 같기도 하고
그래서 아이폰 타이머를 보다가 쿠퍼티노 디자인을 적용해볼까?!라는 생각이 들었다.
void _selectTime() async {
final TimeOfDay? newTime = await showTimePicker(
context: context,
initialTime: _time,
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
child: child!,
);
},
);
if (newTime != null) {
setState(() {
_time = newTime;
});
}
}
소스는 플러터 공식 문서를 참고했다. alwaysUse24HourFormat을 하면 24시간 선택으로 변경되어서 오른쪽에 AM/PM이 사라진다
CupertinoDatePicker
머티리얼이 Android라면~~ 쿠퍼티노는 iOS이다!
CupertinoDatePicker은 날짜 선택의 위젯으로 Date, TIme, DateTime을 선택할 수 있다
선택기의 초기 날짜 또는 시간은 DateTime으로 지정할 수 있다.
그래서 처음에는 year, month, day을 다 0으로 주고 hour랑 minute값만 지정했다.
처음엔 1시간 이상이면 타이머 시간 표기를 시:분:초로 하고 아니면 분:초만 했었다.
그렇게 작업하고 어느 정도 작업이 끝났다고 생각이 들었을 때!
게더에서 알게 된 좋은 분한테 해당 화면을 보여줬었다!
그런데 타이머 설정하는 부분이 헷갈릴 수 있다는 의견을 주셨다.
그 이유는 타이머 설정하는 건 시/분만 나오는데 보이는 건 시/분/초였다가 분/초로 되어버린다고 해서 이다.
계속 개발하는 사람은 익숙하다 보니 헷갈리지 않았는데, 처음 이용하는 사용자로 빙의해서 해보니 헷갈릴 수 있다는 생각이 들었다.
그래서 시/분을 선택하고 분/초만 나오게 했다
예를 들면 1시간 10분을 선택하면 70:00 이렇게 말이다.
CupertinoTimerPicker
문서는 꼼꼼하게 보라는 말이 맞다.
CupertinoTimerPicker은 생각도 못했다가 앱 게시하고 블로그로 정리하려고 CupertinoDatePicker 문서 보던 중 발견한 위젯.
CupertinoDatePicker 작업할 땐 위에 소스 결과 화면만 보고 작업을 해서 See also에 적힌 문장을 못 봤다!!!ㅠㅠㅠ
CupertinoDatePicker이 날짜 및 시간이라고 했지만 CupertinoTimerPicker은 딱 시간만 되는 위젯이다.
CupertinoTimerPicker은 시, 분, 초을 선택할 수 있다!!
그래서 문서에 나온 예제를 보니
와우 hour와 min이 표시가 된다!!! 어쩐지!! 타이머에 보면 시/분/초가 다 보이던데!!!!!
그래도 검토 중이라 다행이다~~ 이러면서 후다닥 적용하고 테스트해보던 중!!
폰트도 NotoSansKR으로 적용했는 데, hour,min 말고 시, 분으로 나오게 할까?!라는 생각이 들어서
어떻게 적용하는지 확인 고고고!
한글 정의하기
사용자가 설정한 언어에 따라 지역별로 정의된 값을 보이게 하는 방법은 localization 패키지를 설치해서 작업해야 한다
flutter pub add localization로 패키지를 설치하고
pubspec.yaml 파일을 아래와 같이 수정해준다
이번 앱에선 i18n을 사용하지 않기 때문에 assets 설정은 제외했다.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
localization: <last-version>
그런 다음 MaterialApp에서 localizationsDelegates과 supportedLocales을 설정해주면
return MaterialApp(
...
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('ko', ''),
Locale('en', ''),
],
);
따라란!! 시간, 분으로 보이기 성공!
타이머 설정 마무리
이렇게 최종으로 타이머를 설정하고 보니 시간, 분이 보이니 이제 타이머 표기법을 바꾸는 게 좋겠다 라는 생각이 들었다.
만약 59분에서 0분으로 넘어갈 때 시간이 +1이 된다면 기존처럼 분:초로 표기하는 게 맞다고 생각이 들지만
현재는 그런 경우가 아니니 맨 처음 표기한 방식으로 변경했다.
타이머 동작하는 방식은 추후 getX 상태 관리에 대해 정리할 때! 다뤄볼 예정입니다!
사운드 재생
타이머가 끝나니 알림음이 필요했다!
음악 재생에 사용한 패키지는 just_audio
그러면 어떤 흐름으로 음악 재생을 했는지 알아보자!
mp3파일을 재생해야 하기 때문에 pubspec.yaml에서 assets/audio를 등록한다
flutter:
assets:
- assets/audio/
그다음 TimerController에서 AudioPlayer()를 인스턴스화 하고
final player = AudioPlayer();
TimerController의 생명주기가 onInit 일 때 asset을 추가하고
await player.setAsset('assets/audio/timer_finish.mp3');
설정한 타이머가 지나면 mp3를 불러오고 실행!
finish() {
player.load();
player.play();
}
이렇게 타이머가 종료하면 알림음이 나오게 하기 완료~!
앱 이름 변경
앱 이름을 모래시계 타이머로 바꿔야겠다라고 생각해서 검색!
stackoverflow 사랑해요
앱 아이콘 만들기
이제 기본 아이콘에서 나름 귀염 뽀짝 한 아이콘을 만들고 싶어졌다.
검색을 해보면 1024x1024 이미지만 있으면 아이콘들을 빠르게 만들어주는 패키지는 flutter_launcher_icons 을 주로 사용한다.
설치하고 pubspec.yaml 에 아래와 같이 설정해준 다음
flutter_icons:
image_path: "assets/icon/icon.png"
android: true
ios: true
flutter pub run flutter_launcher_icons:main를 하면 알아서 뚝딱 해준다고 한다!
하지만 사용해보니 Unhandled exception: FormatException: Invalid number (at character 1) 이 발생했고..
minSdkVersion을 수정해보라는 이야기가 많아해 봤지만 몇 시간 동안 해결되지 않아..
flutter clean, flutter pub get 계속하다가 더 하다간 멘털이 퐈사삭 할 것 같아..
ios만 적용하기로 결심! android: false 처리하고 android 앱 아이콘은 Android Asset Studio 사이트를 이용하여 만들었고./android/app/src/main/res 해당 경로에 넣어줬다
그렇게 해서 완성한 아이콘 ㅎㅎ.. 왼쪽은 처음에 만든 아이콘이다. 모래시계가 별로다! 모래시계가 좀 흘러내렸으면 좋겠다! 등 의견이 많아 오른쪽으로 바꿨다!!
똥손이라 여기까지가 끝인가 보오~~ 그래도 만들었다는 게 중요하지 암암! 나.름.만.족!
자 0.1원이라도 벌고 싶어 구글 광고를 달아보자 (?)
수익이 적더라도 내가 직접 생산해서 벌어보고 싶은 마음에!
구글 광고를 달아보기로 결심!
Google AdMob를 가입하고 모바일 광고 SDK(Flutter) 베타 를 참고해서 적용해봐야지!
우선 앱을 생성하고 광고 단위를 추가해준다!
광고 단위는 생각보다 다양하고 저는 배너로 했어요!
그런 다음 android/app/src/main/AndroidManifest.xml 을 업데이트해줘야 한다
<manifest>
<application>
<!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
<application>
<manifest>
참고로 iOS도 해줘야 iOS에서 실행할 때 에러가 안 납니다!
똑같이 앱에서 iOS 추가해서 광고 단위 추가해주고
ios/Runner/Info.plist 파일에서 GADApplicationIdentifier 키를 추가해줍니다!
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-################~##########</string>
그리고 앱을 실행하기 직전 MobileAds.instance.initialize()를 호출하여 SDK 초기화를 해줍니다!
void main() {
WidgetsFlutterBinding.ensureInitialized();
MobileAds.instance.initialize();
runApp(MyApp());
}
이제 배너를 써야 하니! AdBannerController로 배너를 호출해보겠습니다!
class AdBannerController extends GetxController {
final AppBannerAdManager appBannerAdManager = AppBannerAdManager();
late BannerAd bannerAd;
@override
void onInit() {
super.onInit();
if (Platform.isAndroid) {
appBannerAdManager.loadAd();
bannerAd = appBannerAdManager.showAd();
}
update();
}
BannerAd getBannerAd() {
return bannerAd;
}
}
이렇게 플랫폼이 안드로이드 일 경우에 설정을 해주고
Platform.isAndroid
? SizedBox(
height: 50.0,
child: AdWidget(
ad: adController.getBannerAd(),
),
)
: const SizedBox(
height: 50,
)
UI영역에서 플랫폼이 안드로이드일 때만 배너를 가져왔습니다!
그러면 이렇게!
그럼 광고가 따딴!! 계속 앉아서 그런가 허리 쿠션 광고가...
구글 플레이 콘솔에서 앱을 출시해보자
Android 앱 출시 준비하기!
플러터 공식 문서 Android 앱 출시 준비하기를 따라 작업했습니다
쭈우욱 따라 앱 서명하고 참조하고 구성하고
release 앱 번들 빌드하기는 앱 번들을 추천해서 앱 번들로 작업했습니다!
flutter build appbundle을 실행하면
이 야호!!!! 그러면 해당 경로로 가서 app-release.aab를 등록하면 되는 거구나!! 두근두근하며
구글 플레이 콘솔로 고고!
따란 따란 딴~ 뭔가 드디어 만든다! 이런 기분이 들어서 후다닥 작성했었던
기본!!!
다 쓰기엔 양이 많을 듯하여.. 간단하게 흐름을 보면
대시보드를 따라 하라고 되어 있어서 쭉쭉했습니다
테스트
기억이 흐릿하지만.. 앱 출시 전 자체적으로 테스트를 하라고 안내가 되어 있었습니다!
내부 테스트 -> 버전 만들기 하면 아래와 같은 화면이 뜨는데
여기에 아까 App Bundle한 app-release.aab을 드롭하면
검토하면서 하는데
처음에 본 오류 "com.example'은(는) 제한되어 있으므로 다른 패키지 이름을 사용해야 합니다" ㅋㅋㅋㅋ!!!ㅋㅋ ㅠㅠ!!
기본으로 플러터 앱을 구현했더니.. 저런 일이! 그래서 후다닥 바꾸고 다시 등록!
참고로 flutter create --org com.domain appName 으로 하면 알아서 패키지 다 바꿔준다!
그다음 발견한 오류는 "앱의 타겟팅 API 수준을 30 이상으로 변경하세요"..!
2021년 11월부터 앱 업데이트는 API 수준 30 이상을 타겟팅 해야한다고 나와있다.
그래서 build.gradle 파일에서 minSdkVersion 21로 targetSdkVersion 30로 설정했다.
그런 다음 등록하니 된다 된다!!!
버전을 등록하면 테스터도 입력해야겠죠?!
최대 100명까지 내부 테스트를 참여할 수 있다고 나와있습니다!
그래서 호로록 이메일을 추가해주고 "테스터는 웹에서 테스트에 참여할 수 있습니다."라고 하면서 링크를 제공해줍니다
그러면 링크를 보내주면 테스트 앱 설치할 수 있고 사용해 볼 수 있다!
나만 그런진 모르겠지만 앱 등급 분류를 안 한 상태에서 했더니 등급이 17세 이상이었다.
그래서 다들 흠짓!
이렇게 하고 대시보드 따라 앱 기본 정보 입력하고, 앱 등급 분류하고(3세 이상 나옴), 개인정보는 만 13세 이상이면 안 해도 된다 하여 안 하고!
테스트해보니 아쉬운 점 발견!
언니 폰으로 테스트를 해보니 아쉬운 부분을 발견했다.
- 자동 잠금을 하면 화면이 꺼진다
-> 계속 켜놓는 기능이 필요하겠군 - 앱 종료가 아닌 앱에서 나가면 모래시계도 멈춰버린다
-> 백그라운드 프로세스를 적용해봐야겠다 - UI
-> 이때는 분/초가 보이지 않아 뭔가 아쉬움
-> 디자인은 어쩔 수 없지, 우선 가볍게 해 보자로 타협..
자동 잠금 멈춰
wakelock 패키지를 사용하면 화면이 자동으로 꺼지는 것을 방지하는 화면 깨우기 잠금을 활성화 및 토글 할 수 있다.
기본적으로 계속 깨어 있는 상태로 유지할 수 있다
랄라 그러면 해봐야죵!
해당 패키지를 설치한다 (flutter pub add wakelock)
컨트롤러의 생명 주기가 onInit일 때 웨이크락을 활성화해줍니다
@override
onInit() async {
...
//웨이크락 활성화
Wakelock.enable();
}
컨트롤러의 생명 주기가 onClose()일 때 웨이크락을 비활성화해줍니다
@override
onClose() {
...
WidgetsBinding.instance.removeObserver(this);
Wakelock.disable();
}
그런 다음 스위치 값에 따라 웨이크락 활성/비활성화 처리를 위해 메소드를 생성합니다
onWakeLock(bool value) {
wakeLock.value = value;
wakeLock.value ? Wakelock.enable() : Wakelock.disable();
}
UI에서는 wakeLock의 변화를 감지합니다
Obx(
() => Switch(
value: controller.wakeLock.value,
onChanged: (value) {
controller.onWakeLock(value);
},
activeColor: Theme.of(context).colorScheme.primaryContainer,
focusColor: Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.onPrimary,
),
),
AppBar actions에 Switch으로 추가할지 아니면 시간 아래에 SwitchListTile로 할지 고민했었는데
게더에서 대화를 하다 보니 어떻게 해야 할지 감이 와서 오른쪽 상단에 추가했다!
앱을 나가도 흘러가는 모래시계
TimerController에 with WidgetsBindingObserver 를 추가해서 WidgetsBindingObserver를 사용해 앱의 상태 변화를 추적합니다
TimerController의 생명 주기가 onInit 일 때 옵서버를 추가해줍니다
@override
onInit() async {
...
WidgetsBinding.instance.addObserver(this);
}
TimerController의 생명 주기가 onCloe일 땐 옵저버를 제거해주고 웨이크락도 비활성화해줍니다.
@override
onClose() {
...
WidgetsBinding.instance.removeObserver(this);
Wakelock.disable();
}
AppLifecycleState 문서에서 생명 주기 종류를 확인할 수 있습니다
이렇게 설정하고 앱을 나갔다가 들어와도 모래시계가 흘러가는 것을 확인!
이제 출시만 하면 되나!

내부 테스트를 통해 계속 버전을 올리다 보니 버전 코드 7까지 올라갔다!
(참고로 같은 버전은 계속 사용할 수 없으며 pubspec.yaml 파일에서 버전을 계속 올려야 한다)
그러다가 프로덕션 출시할 수 있어서 바로 등록해봤다!
이야호
마치며
아직 검토지만 인생 첫 앱 출시!
최근 기록하기로 했습니다.라는 책을 본 영향 때문인지
진짜 이건 기록으로 남기고 싶어서 희미한 기억이 소멸되기 전에 블로그 작성을 시작했다.
새벽 2시부터 블로그 글쓰기 버튼을 눌러 작성했는데 벌써 5시가 넘었다.
블로그 쓰다 보니 관련 문서를 한 번 더 보게 되었고, 그러다가 ui도 개선하게 되었다.
어제 새벽부터 지금까지 재미있게 달려서 시간이 빨리 지나갔다.
작성하다 보니 더 정리하고 싶은 부분도 생겨서 임시 저장으로 타이틀만 적었는데 이 부분도 해야지~!
야호 다들 굿밤!!!
'IT > 기록' 카테고리의 다른 글
[Flutter] macOS Connection failed (OS Error: Operation not permitted, errno = 1) (0) | 2022.06.09 |
---|---|
[Vue] Mac에서 Vue-Cli 설치하기 (0) | 2022.06.09 |
[Flutter] 화면 전환 (0) | 2022.06.02 |
[JPA] Entity Validation - null (0) | 2022.05.31 |
[Flutter] ColorScheme class - 주요 색상만 지정하면 색깔을 알아서 뚝딱? (0) | 2022.05.20 |