플러터(Flutter)는 구글에서 만든 크로스 플랫폼 프레임워크다.
이번 글에서는 Plugin sensors_plus, shake를 이용하여 핸드폰을 흔들면 Random 숫자를 보여주는 주사위 APP를 만들어 봅니다.
sensors_plus 패키지(package)와 shake 패키지
sensors_plus 패키지는 핸드폰의 가속도계와 자이로스코프 센서를 편리하게 사용할 수 있게 해줍니다.
하지만 이 패키지만으로는 핸드폰을 흔들었다고 판단할 수 있는 수학적 로직을 알기가 어렵기에 핸드폰을 흔든 정도에 따라 처리를 쉽게 할 수 있게 해주는 shake 패키지를 함께 사용합니다. 아래와 같이 터미널에서 flutter pub add 명령으로 간편하게 추가 가능합니다.
// senseors_plus package 추가 명령
$ flutter pub add sensors_plus
// shake package 추가 명령
$ flutter pub add shake
Random Dice 프로젝트(Project) 파일 구조

상수(constant) 정의
‘const’폴더와 colors.dart 파일을 추가하여 프로젝트에서 사용할 상수 값들을 정의해 주었다.
colors.dart
import 'package:flutter/material.dart';
const backgroundColor = Color(0xFF0E0E0E);
const primaryColor = Colors.white;
final secondaryColor = Colors.grey[600]; // [600] = shade(음영 단계)
이미지 추가
asset과 img 폴더를 추가하고 주사위 이미지로 쓸 1~6.png를 추가해 줍니다.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
sensors_plus: ^6.1.1
shake: ^3.0.0
...
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- asset/img/
위와 같이 작성 후 터미널이나 안드로이드 스튜디오에서 pub get 명령을 실행하여 적용해 줍니다.
$ flutter pub get
화면(Screen) 소스 코드(Source Code)
main.dart
import 'package:flutter/material.dart';
import 'package:random_dice/const/colors.dart';
import 'package:random_dice/screen/root_screen.dart';
void main() {
runApp(
MaterialApp(
title: 'Random Dice',
theme: ThemeData(
scaffoldBackgroundColor: backgroundColor,
sliderTheme: SliderThemeData( // Slider 위젯 테마
thumbColor: primaryColor,
activeTrackColor: primaryColor,
inactiveTrackColor: primaryColor.withValues(alpha: 0.3),
),
// BottomNavigationBar 위젯 테마
bottomNavigationBarTheme: BottomNavigationBarThemeData(
selectedItemColor: primaryColor, // 선택 색상
unselectedItemColor: secondaryColor, // 비선택 색상
backgroundColor: backgroundColor, // 배경 색상
),
),
home: RootScreen(),
)
);
}
root_screen.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';
import 'package:random_dice/screen/settings_screen.dart';
import 'package:shake/shake.dart';
class RootScreen extends StatefulWidget {
const RootScreen({super.key});
@override
State<RootScreen> createState() => _RootScreenState();
}
// vsync 기능이 필요하기 때문에 TickerProviderMinin을 minin으로 제공해야해서 with TickerProviderStateMixin
class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin {
TabController? tabController;
double threshold = 2.7;
int number = 1;
ShakeDetector? shakeDetector;
@override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
tabController!.addListener(tabListener);
shakeDetector = ShakeDetector.autoStart( // 핸드폰 흔들기 감지 센서 디텍터 시작
shakeSlopTimeMS: 100, // 감지 주기
shakeThresholdGravity: threshold, // 핸드폰 흔들기 감지 민감도 값 매개변수
onPhoneShake: onPhoneShake, // 핸드폰 흔들기 감지시 콜백 함수 매개변수
);
}
void onPhoneShake(event) {
final rand = Random();
setState(() {
number = rand.nextInt(5) + 1;
});
}
tabListener() {
setState(() {});
}
@override
void dispose() {
tabController!.removeListener(tabListener);
shakeDetector!.stopListening();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: TabBarView(controller: tabController, children: renderChildren()), // 탭 화면
bottomNavigationBar: renderBottomNavigation(), // 하단 탭 선택에 따른 렌더링 및 화면처리 구현 매개변수
);
}
List<Widget> renderChildren() {
return [
// Center(
// child: Text('Tab 1', style: TextStyle(color: Colors.white)),
// ),
HomeScreen(number: number), // 주사위가 있는 홈 스크린 페이지
// Center(
// child: Text('Tab 2', style: TextStyle(color: Colors.white)),
// ),
SettingsScreen( // 핸드폰 흔들기 민감도 설정 스크린 페이지
threshold: threshold,
onThresholdChange: onThresholdChange,
),
];
}
void onThresholdChange(double val) {
setState(() {
threshold = val;
});
}
BottomNavigationBar renderBottomNavigation() {
// 탭 내비게이션 구현
return BottomNavigationBar(
currentIndex: tabController!.index,
onTap: (int index) {
setState(() {
tabController!.animateTo(index);
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.edgesensor_high_outlined),
label: 'Dice',
),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Setting'),
],
);
}
}
위 코드에서 vsync 기능을 사용하기 위해 TickerProviderStateMinin을 사용했습니다.
TickerProviderMinin과 SingleTickerProviderMinin은 애니메이셔녀 효율을 올려주는 역할을 합니다.
플러터(Flutter)는 기기가 지원하는 대로 60FPS(초당 60프레임)부터 120FPS를 지원하는데 TickerPrividerMinin을 사용하면 정확히 한 틱(1FPS)마다 애니메이션을 실행합니다. 간혹 애니메이션 코드를 작성하다 보면 실제로 화면에 그릴 수있는 주기보다 더 자주 렌더링을 실행하게 될 때가 있는데 TickerProviderMixin을 사용하면 이런 비효율적인 상황을 막아줍니다. TabController도 vsync에 TickerProviderMixin을 제공함으로서 렌더링 효율을 극대화할 수 있습니다.
home_screen.dart
import 'package:flutter/material.dart';
import 'package:random_dice/const/colors.dart';
class HomeScreen extends StatelessWidget {
final int number;
const HomeScreen({required this.number, Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Image.asset('asset/img/$number.png')),
SizedBox(height: 32.0),
Text(
'Lucky Number',
style: TextStyle(
color: secondaryColor,
fontSize: 20.0,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 12.0),
Text(
number.toString(),
style: TextStyle(
color: primaryColor,
fontSize: 60.0,
fontWeight: FontWeight.w200,
),
),
],
);
}
}
settings_screen.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:random_dice/const/colors.dart';
class SettingsScreen extends StatelessWidget {
final double threshold;
// Slider 변경 시에 호출되는 함수
final ValueChanged<double> onThresholdChange;
const SettingsScreen({
super.key,
required this.threshold,
required this.onThresholdChange,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Row(
children: [
Text(
'Sensitive',
style: TextStyle(
color: secondaryColor,
fontSize: 20.0,
fontWeight: FontWeight.w700,
),
),
],
),
),
Slider(
min: 0.1,
max: 10.0,
divisions: 101, // 최소값(min)과 최대값(max) 사이의 구간의 개수
value: threshold, // 슬라이더 값
onChanged: onThresholdChange, // 값 변경 시 호출 함수
label: threshold.toStringAsFixed(1), // 슬라이더 라벨 표시값 (소수점 1자리까지 표시 설정)
),
],
);
}
}
Summary
이번 예제는 자이로 센서가 장착되어 있는 실제 핸드폰에서 테스트가 가능합니다.
캐쉬워크와 같은 APP 정도는 이제 플러터(Flutter)로 금방 만들어 낼 수 있을 것 같습니다. 😁
참고
Flutter 기본 기능 | as, show, hide | 변수나 함수, 클래스명이 같은 경우 해결 방법 | example