14. 오늘도 출첵 :: 구글 지도, Geolocator 플러그인, 다이얼로그
14.1 사전 지식
14.1.1 Geolocator 플러그인
- 지리와 관련된 기능을 쉽게 사용할 수 있는 플러그인
- 위치 서비스 권한 확인 _ 위치 서비스 기능이 켜졌는지 확인
final isLocationEnabled = await Geolocator.isLocationServiceEnabled();
- 앱에서 위치 서비스 기능을 사용할 수 있는지 확인
final checkedPermission = await Geolocator.checkPermission(); // 권한 확인
final checkedPermission = await Geolocator.requestPermission(); // 권한 요청
- 현재 위치 지속적으로 반환받기 (getPositionStream 함수)
Geolocator.getPositionStream().listen((Position position) {
print(position);
});
- 두 위치 간의 거리 구하기
// 두 위치 간의 거리를 double 값으로 반환
final distance = Geolocator.distanceBetween(
sLat, // 시작점 위도
sLng, // 시작점 경도
eLat, // 끝지점 위도
eLng, // 끝지점 경도
);
14.2 사전 준비
14.2.1 구글 지도 API 키 입력받기
- 구글 클라우드 플랫폼 접속 + 회원가입
- Maps SDK for Android 사용 설정

14.3 레이아웃 구상하기
- AppBar / Body / Footer

14.4 구현하기
- main.dart
import 'package:chool_check/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
- home_screen.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng( // ➊ 지도 초기화 위치
37.5233273, // 위도
126.921252, // 경도
);
static final Marker marker = Marker(
markerId: MarkerId('company'),
position: companyLatLng,
);
static final Circle circle = Circle(
circleId: CircleId('choolCheckCircle'),
center: companyLatLng, // 원의 중심이 되는 위치. LatLng값을 제공합니다.
fillColor: Colors.blue.withOpacity(0.5), // 원의 색상
radius: 100, // 원의 반지름 (미터 단위)
strokeColor: Colors.blue, // 원의 테두리 색
strokeWidth: 1, // 원의 테두리 두께
);
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
future: checkPermission(),
builder: (context, snapshot) {
// ❶ 로딩 상태
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
// ➋ 권한 허가된 상태
if(snapshot.data == '위치 권한이 허가 되었습니다.'){
// 기존 Column 위젯 관련 코드
return Column(
children: [
Expanded( // 2/3만큼 공간 차지
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
myLocationEnabled: true,
markers: Set.from([marker]),
circles: Set.from([circle]),
),
),
Expanded( // 1/3만큼 공간 차지
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon( // 시계 아이콘
Icons.timelapse_outlined,
color: Colors.blue,
size: 50.0,
),
const SizedBox(height: 20.0),
ElevatedButton( // [출근하기] 버튼
onPressed: () async {
final curPosition = await Geolocator.getCurrentPosition(); // 현재 위치
final distance = Geolocator.distanceBetween(
curPosition.latitude, // 현재위치 위도
curPosition.longitude, // 현재위치 경도
companyLatLng.latitude, // 회사위치 위도
companyLatLng.longitude, // 회사위치 경도
);
bool canCheck =
distance < 100; // 100미터 이내에 있으면 출근 가능
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('출근하기'),
// 출근 가능 여부에 따라 다른 메시지 제공
content: Text(
canCheck ? '출근을 하시겠습니까?' : '출근할수 없는 위치입니다.',
),
actions: [
TextButton(
// 취소를 누르면 false 반환
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('취소'),
),
if (canCheck) // 출근 가능한 상태일 때만 [출근하기] 버튼 제공
TextButton(
// 출근하기를 누르면 true 반환
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
),
],
);
},
);
},
child: Text('출근하기!'),
),
],
),
),
],
);
}
// ➌ 권한 없는 상태
return Center(
child: Text(
snapshot.data.toString(),
),
);
}
),
);
}
AppBar renderAppBar() {
// AppBar를 구현하는 함수
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
Future<String> checkPermission() async {
final isLocationEnabled = await Geolocator.isLocationServiceEnabled(); // 위치 서비스 활성화여부 확인
if (!isLocationEnabled) { // 위치 서비스 활성화 안 됨
return '위치 서비스를 활성화해주세요.';
}
LocationPermission checkedPermission = await Geolocator.checkPermission(); // 위치 권한 확인
if (checkedPermission == LocationPermission.denied) { // 위치 권한 거절됨
// 위치 권한 요청하기
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
// 위치 권한 거절됨 (앱에서 재요청 불가)
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 설정에서 허가해주세요.';
}
// 위 모든 조건이 통과되면 위치 권한 허가완료
return '위치 권한이 허가 되었습니다.';
}
}
15. 포토 스티커 :: GestureDetector
15.1 사전 지식
15.1.1 GestureDetector와 제스처
- GestureDetector : 플러터에서 지원하는 모든 제스처들을 구현할 수 있는 위젯

15.2 사전 준비
- 1. 안드로이드 에뮬레이터 가상머신에 이미지 추가
- 2. 이미지와 폰트 추가
- 3. pubspec.yaml 설정하기 & 네이티브 권한 설정하기
dependencies:
flutter:
sdk: flutter
cupertino_icons: “1.0.8
image_picker: 1.1.2
image_gallery_saver_plus: 3.0.5
uuid: 4.5.1
flutter:
uses-material-design: true
assets:
- asset/img/
15.3 레이아웃 구상하기
- 1. 홈 스크린 위젯

- 2. 이미지 선택 후 화면

15.4 구현하기
- main.dart
import 'dart:io';
import 'package:image_editor/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
- home_screen.dart
import 'package:flutter/material.dart';
import 'package:image_editor/component/main_app_bar.dart';
import 'package:image_editor/model/sticker_model.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:image_editor/component/footer.dart';
import 'package:image_editor/model/sticker_model.dart';
import 'package:image_editor/component/emoticon_sticker.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'dart:typed_data';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
XFile? image; // 선택한 이미지를 저장할 변수
Set<StickerModel> stickers = {}; // 화면에 추가된 스티커를 저장할 변수
String? selectedId; // 현재 선택된 스티커의 ID
GlobalKey imgKey = GlobalKey();
// 미리 생성해둔 onPickImage() 함수 변경하기
void onPickImage() async {
final image = await ImagePicker()
.pickImage(source: ImageSource.gallery); // ➊ 갤러리에서 이미지 선택하기
setState(() {
this.image = image; // 선택한 이미지 변수에 저장하기
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
// ➊ 스크린에 Body, AppBar, Footer 순서로 쌓을 준비
children: [
renderBody(),
Positioned(
top: 0,
left: 0,
right: 0,
child: MainAppBar( // ➋ AppBar 위치하기
onPickImage: onPickImage,
onSaveImage: onSaveImage,
onDeleteItem: onDeleteItem,
),
),
if (image != null) // ➌ image가 선택되면 Footer 위치하기
Positioned( // 맨 아래에 Footer 위젯 위치하기
bottom: 0,
left: 0, // left와 right를 모두 0을 주면 좌우로 최대 크기를 차지함
right: 0,
child: Footer(
onEmoticonTap: onEmoticonTap,
),
),
],
),
);
}
Widget renderBody() {
if (image != null) {
return RepaintBoundary(
// ➊ 위젯을 이미지로 저장하기 위해 사용
key: imgKey,
child: Positioned.fill(
child: InteractiveViewer(
child: Stack(
fit: StackFit.expand,
children: [
Image.file(
File(image!.path),
fit: BoxFit.cover,
),
...stickers.map(
(sticker) => Center(
child: EmoticonSticker(
key: ObjectKey(sticker.id),
onTransform: () {
onTransform(sticker.id);
},
imgPath: sticker.imgPath,
isSelected: selectedId == sticker.id,
),
),
),
],
),
),
),
);
} else {
// ➋ 이미지 선택이 안 된 경우 이미지 선택 버튼 표시
return Center(
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
),
onPressed: onPickImage,
child: Text('이미지 선택하기'),
),
);
}
}
void onEmoticonTap(int index) async {
setState(() {
stickers = {
...stickers,
StickerModel(
id: Uuid().v4(), // ➊ 스티커의 고유 ID
imgPath: 'asset/img/emoticon_$index.png',
),
};
});
}
void onSaveImage() async {
RenderRepaintBoundary boundary = imgKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(); // ➊ 바운더리를 이미지로 변경
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); // ➋ byte data 형태로 형태 변경
Uint8List pngBytes = byteData!.buffer.asUint8List(); // ➌ Unit8List 형태로 형태 변경
await ImageGallerySaverPlus.saveImage(pngBytes, quality: 100);
ScaffoldMessenger.of(context).showSnackBar( // ➋ 저장 후 Snackbar 보여주기
SnackBar(
content: Text('저장되었습니다!'),
),
);
}
void onDeleteItem() async {
setState(() {
stickers = stickers.where((sticker) => sticker.id != selectedId).toSet(); // ➊ 현재 선택돼 있는 스티커 삭제 후 Set로 변환
});
}
void onTransform(String id){ // 스티커가 변형될 때마다 변형 중인 스티커를 현재 선택한 스티커로 지정
setState(() {
selectedId = id;
});
}
}
- sticker_model.dart
class StickerModel {
final String id;
final String imgPath;
StickerModel({
required this.id,
required this.imgPath,
});
@override
bool operator ==(Object other) { // ➊ ==로 같은지 비교할 때 사용되는 로직
return (other as StickerModel).id == id; // ID값이 같은 인스턴스끼리는 같은 스티커로 인식
}
// ➋ Set에서 중복 여부를 결정하는 속성
// ID값이 같으면 Set 안에서 같은 인스턴스로 인식
@override
int get hashCode => id.hashCode;
}
- emotion_sticker.dart
import 'package:flutter/material.dart';
class EmoticonSticker extends StatefulWidget {
// ➊ 스티커를 그리는 위젯
final VoidCallback onTransform; // ➏
final String imgPath; // ➋ 이미지 경로
final bool isSelected;
const EmoticonSticker({
required this.onTransform,
required this.imgPath,
required this.isSelected,
Key? key,
}) : super(key: key);
@override
State<EmoticonSticker> createState() => _EmoticonStickerState();
}
class _EmoticonStickerState extends State<EmoticonSticker> {
// 확대/축소 배율
double scale = 1;
// 가로의 움직임
double hTransform = 0;
// 세로의 움직임
double vTransform = 0;
// 위젯의 초기 크기 기준 확대/축소 배율
double actualScale = 1;
@override
Widget build(BuildContext context) {
return Transform(
// ➊ child 위젯을 변형할 수 있는 위젯
transform: Matrix4.identity()
..translate(hTransform, vTransform) // ➋ 상/하 움직임 정의
..scale(scale, scale), // ➌ 확대/축소 정의
// 기존 작성해둔 Container 위젯
child: Container(
decoration: widget.isSelected // ➊ 선택 상태일 때만 테두리 색상 구현
? BoxDecoration(
borderRadius: BorderRadius.circular(4.0), // 모서리 둥글게
border: Border.all(
// 테두리 파란색
color: Colors.blue,
width: 1.0,
),
)
: BoxDecoration(
// 테두리는 투명이나 너비는 1로 설정해서 스티커가 선택 취소될 때 깜빡이는 현상 제거
border: Border.all(
width: 1.0,
color: Colors.transparent,
),
),
// 기존에 작성해둔 GestureDetector
child: GestureDetector(
onTap: () {
// ➌ 스티커를 눌렀을 때 실행할 함수
widget.onTransform(); // ➏ 스티커의 상태가 변경될 때마다 실행
},
onScaleUpdate: (ScaleUpdateDetails details) {
// ➍ 스티커의 확대 비율이 변경됐을 때 실행
setState(() {
scale =
details.scale * actualScale; // ➋ 최근 확대 비율 기반으로 실제 확대 비율 계산
vTransform += details.focalPointDelta.dy; // ➌ 세로 이동 거리 계산
hTransform += details.focalPointDelta.dx; // ➌ 가로 이동 거리 계산
});
},
onScaleEnd: (ScaleEndDetails details) {
actualScale = scale; // ➊ 확대 비율 저장
}, // ➎ 스티커의 확대 비율 변경이 완료됐을 때 실행
child: Image.asset(
widget.imgPath, // ➋
),
),
),
);
}
}
- footer.dart
import 'package:flutter/material.dart';
// ➋ 스티커를 선택할 때마다 실행할 함수의 시그니처
typedef OnEmoticonTap = void Function(int id);
class Footer extends StatelessWidget {
final OnEmoticonTap onEmoticonTap;
const Footer({
required this.onEmoticonTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white.withOpacity(0.9),
height: 150,
child: SingleChildScrollView( // ➊ 가로로 스크롤 가능하게 스티커 구현
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
7,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: GestureDetector(
onTap: () {
onEmoticonTap(index + 1); // ➌ 스티커 선택할 때 실행할 함수
},
child: Image.asset(
'asset/img/emoticon_${index + 1}.png',
height: 100,
),
),
),
),
),
),
);
}
}
- main_app_bar.dart
import 'package:flutter/material.dart';
class MainAppBar extends StatelessWidget {
final VoidCallback onPickImage; // 이미지 선택 버튼 눌렀을 때 실행할 함수
final VoidCallback onSaveImage; // 이미지 저장 버튼 눌렀을 때 실행할 함수
final VoidCallback onDeleteItem; // 이미지 삭제 버튼 눌렀을 때 실행할 함수
const MainAppBar({
required this.onPickImage,
required this.onSaveImage,
required this.onDeleteItem,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
IconButton( // ➊ 이미지 선택 버튼
onPressed: onPickImage,
icon: Icon(
Icons.image_search_outlined,
color: Colors.grey[700],
),
),
IconButton( // ➋ 스티커 삭제 버튼
onPressed: onDeleteItem,
icon: Icon(
Icons.delete_forever_outlined,
color: Colors.grey[700],
),
),
IconButton( // ➌ 이미지 저장 버튼
onPressed: onSaveImage,
icon: Icon(
Icons.save,
color: Colors.grey[700],
),
),
],
),
);
}
}
16. AI 채팅봇, 소울챗 :: HTTP 요청, Rest API, JSON, 제미나이 API, Isar 데이터베이스
16.1 사전 지식
16.1.1 HTTP 요청
- HTTP : TCP / UDP 방식을 사용, 통신 프로토콜
- 서버 : HTTP 요청을 보냄 / 클라이언트 : HTTP 요청을 받음

- HTTP 요청 URL 의 형태

- HTTP 기반 API 종류 : REST API / GraphQL / gRPC
- HTTP 요청의 종류
- GET : 서버로부터 데이터를 가져옴
- POST : 데이터를 서버에 저장함
- PUT : 데이터를 업데이트
- DELETE : 데이터를 삭제
16.1.2 REST API
- REST 기준을 따르는 HTTP API. (4가지의 기준을 준수해서 RESTful API)

- REST API의 4가지 기준 : 균일한 인터페이스 / 무상태 / 계층화된 시스템 / 캐시(Cache)
16.1.3 JSON
- body를 구성할 때 사용하는 구조 XML, JSON
- JSON : 인간이 읽을 수 있는 텍스트를 사용해 키-값 쌍으로 이뤄진 데이터 객체를 전달하는 개방형 표준 포맷
- 플러터에서 JSON 구조로 된 데이터 응답받으면 직렬화 Senalzation 통해 클래스 인스턴스로 변환해 사용 가능
16.1.4 제미나이 API
- 제미나이(Gemini) : 알파고를 개발한 딥마인드가 개발한 AI
- 제미나이의 특징 : 멀티모달 이해 능력 / 높은 추론 능력과 정확성 / 최적화된 언어 모델 / 안전성과 윤리적 사용 / 사용자 친화적 인터페이스 및 확장성

16.1.5 Isar 데이터베이스
- Isar 데이터베이스 : 플러터에서 사용할 수 있는 고성능 NoSQL 데이터베이스
- NoSOL : 구조 강제 X => 사용이 자유로움 but 프로젝트가 커졌을 때 유지보수 어려워질 수 있음
- 론트엔드 데이터베이스 : 퍼포먼스/사용성이 조금 더 중요시, 구조 복잡X => NoSQL 데이터베이스 사용하는 경우 많음
- 컬렉션 정의하기 :: Isar DB는 데이터를 컬렉션 단위로 나눔. (컬렉션 : 특정 자료형 객체를 모아 관리하는 개념)
import 'package:isar/isar.dart';
// [파일 이름].g.dart 파일이 build_runner에 의 해 생성됨
part 'message_model.g.dart';
// 클래스를 @colLection으로 어노테이트해주면 Isar가 자동 인식
@collection
class MessageModel {
Id id = Isar.autoIncrement;
bool isMine;
String message;
int? point;
DateTime date;
MessageModel ({
this.point,
required this.isMine,
required this.message,
required this.date,
});
}
16.2 사전 준비
- 1. 제미나이 API 키 발급받기
- 2. 이미지/폰트 추가
- 3. 안드로이드 네이티브 & pubspec.yaml 설정하기
- 4. 프로젝트 초기화하기
16.3 레이아웃 구상하기

16.4 구현하기
- main.dart
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:soul_talk/model/message_model.dart';
import 'package:soul_talk/screen/home_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
[MessageModelSchema],
directory: dir.path,
);
GetIt.I.registerSingleton<Isar>(isar);
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
- home_screen.dart
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:isar/isar.dart';
import 'package:soul_talk/component/chat_text_field.dart';
import 'package:soul_talk/component/date_divider.dart';
import 'package:soul_talk/component/logo.dart';
import 'package:soul_talk/component/message.dart';
import 'package:soul_talk/model/message_model.dart';
final sampleData = [
MessageModel(
id: 1,
isMine: true,
message: '오늘 저녁으로 먹을 만한 메뉴 추천해줘!',
point: 1,
date: DateTime(2024, 11, 23),
),
MessageModel(
id: 2,
isMine: false,
message: '칼칼한 김치찜은 어때요!?',
point: null,
date: DateTime(2024, 11, 23),
),
];
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
bool isRunning = false;
String? error;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Expanded(
child: StreamBuilder<List<MessageModel>>(
stream: GetIt.I<Isar>().messageModels.where().watch(fireImmediately: true),
builder: (context, snapshot) {
final messages = snapshot.data ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) async => scrollToBottom());
return buildMessageList(messages);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 32.0),
child: ChatTextField(
error: error,
loading: isRunning,
onSend: handleSendMessage,
controller: controller,
),
),
],
),
),
);
}
void scrollToBottom() {
if (scrollController.position.pixels != scrollController.position.maxScrollExtent) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
handleSendMessage() async {
if (controller.text.isEmpty) {
setState(() => error = '메세지를 입력해주세요!');
return;
}
int? currentModelMessageId;
int? currentUserMessageId;
final isar = GetIt.I<Isar>();
final currentPrompt = controller.text;
try {
setState(() {
isRunning = true;
});
controller.clear();
final myMessagesCount = await isar.messageModels.filter().isMineEqualTo(true).count();
currentUserMessageId = await isar.writeTxn(() async {
return await isar.messageModels.put(
MessageModel(
isMine: true,
message: currentPrompt,
point: myMessagesCount + 1,
date: DateTime.now(),
),
);
});
final contextMessages = await isar.messageModels.where().limit(5).findAll();
final List<Content> promptContext = contextMessages
.map(
(e) => Content(
e.isMine! ? 'user' : 'model',
[
TextPart(e.message!),
],
),
)
.toList();
final model = GenerativeModel(
model: 'gemini-1.5-flash',
apiKey: 'AIzaSyAnPsqr3-4UhRi-Plw1bDo4QkThuQTFtBg',
systemInstruction:
Content.system('너는 이제부터 착하고 친절한 친구의 역할을 할거야. 앞으로 채팅을 하면서 긍정적인 말만 할 수 있도록 해줘. 이 메세지는 기억만 하고 여기엔 대답할 필요 없어.'),
);
String message = '';
model.generateContentStream(promptContext).listen(
(event) async {
if (event.text != null) {
message += event.text!;
}
final MessageModel model = MessageModel(
isMine: false,
message: message,
date: DateTime.now(),
);
if (currentModelMessageId != null) {
model.id = currentModelMessageId!;
}
currentModelMessageId = await isar.writeTxn<int>(() => isar.messageModels.put(model));
},
onDone: () => setState(() {
isRunning = false;
}),
onError: (e) async {
await isar.writeTxn(() async {
return isar.messageModels.delete(currentUserMessageId!);
});
setState(() {
error = e.toString();
isRunning = false;
});
},
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
Widget buildMessageList(List<MessageModel> messages) {
return ListView.separated(
controller: scrollController,
itemCount: messages.length + 1,
itemBuilder: (context, index) => index == 0
? buildLogo()
: buildMessageItem(
message: messages[index - 1],
prevMessage: index > 1 ? messages[index - 2] : null,
index: index - 1,
),
separatorBuilder: (_, __) => const SizedBox(height: 16.0),
);
}
Widget buildLogo() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: const Padding(
padding: EdgeInsets.only(bottom: 60.0),
child: Logo(),
),
);
}
Widget buildMessageItem({
MessageModel? prevMessage,
required MessageModel message,
required int index,
}) {
final isMine = message.isMine!;
final shouldDrawDateDivider = prevMessage == null || shouldDrawDate(prevMessage.date!, message.date!);
return Column(
children: [
if (shouldDrawDateDivider)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: DateDivider(date: message.date!),
),
Padding(
padding: EdgeInsets.only(left: isMine ? 64.0 : 16.0, right: isMine ? 16.0 : 64.0),
child: Message(
alignLeft: !isMine,
message: message.message!.trim(),
point: message.point,
),
),
],
);
}
bool shouldDrawDate(DateTime date1, DateTime date2) {
return getStringDate(date1) != getStringDate(date2);
}
String getStringDate(DateTime date) {
return '${date.year}년 ${date.month}월 ${date.day}일';
}
}
- message_model.dart
import 'package:isar/isar.dart';
part 'message_model.g.dart';
@collection
class MessageModel {
Id id = Isar.autoIncrement;
bool isMine;
String message;
int? point;
DateTime date;
MessageModel({
required this.isMine,
required this.message,
required this.date,
this.id = Isar.autoIncrement,
this.point,
});
}
- message_model.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message_model.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetMessageModelCollection on Isar {
IsarCollection<MessageModel> get messageModels => this.collection();
}
const MessageModelSchema = CollectionSchema(
name: r'MessageModel',
id: -902762555029995869,
properties: {
r'date': PropertySchema(
id: 0,
name: r'date',
type: IsarType.dateTime,
),
r'isMine': PropertySchema(
id: 1,
name: r'isMine',
type: IsarType.bool,
),
r'message': PropertySchema(
id: 2,
name: r'message',
type: IsarType.string,
),
r'point': PropertySchema(
id: 3,
name: r'point',
type: IsarType.long,
)
},
estimateSize: _messageModelEstimateSize,
serialize: _messageModelSerialize,
deserialize: _messageModelDeserialize,
deserializeProp: _messageModelDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _messageModelGetId,
getLinks: _messageModelGetLinks,
attach: _messageModelAttach,
version: '3.1.0+1',
);
int _messageModelEstimateSize(
MessageModel object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.message.length * 3;
return bytesCount;
}
void _messageModelSerialize(
MessageModel object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeDateTime(offsets[0], object.date);
writer.writeBool(offsets[1], object.isMine);
writer.writeString(offsets[2], object.message);
writer.writeLong(offsets[3], object.point);
}
MessageModel _messageModelDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = MessageModel(
date: reader.readDateTime(offsets[0]),
id: id,
isMine: reader.readBool(offsets[1]),
message: reader.readString(offsets[2]),
point: reader.readLongOrNull(offsets[3]),
);
return object;
}
P _messageModelDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readDateTime(offset)) as P;
case 1:
return (reader.readBool(offset)) as P;
case 2:
return (reader.readString(offset)) as P;
case 3:
return (reader.readLongOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _messageModelGetId(MessageModel object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _messageModelGetLinks(MessageModel object) {
return [];
}
void _messageModelAttach(
IsarCollection<dynamic> col, Id id, MessageModel object) {
object.id = id;
}
extension MessageModelQueryWhereSort
on QueryBuilder<MessageModel, MessageModel, QWhere> {
QueryBuilder<MessageModel, MessageModel, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension MessageModelQueryWhere
on QueryBuilder<MessageModel, MessageModel, QWhereClause> {
QueryBuilder<MessageModel, MessageModel, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
upper: id,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterWhereClause> idNotEqualTo(
Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<MessageModel, MessageModel, QAfterWhereClause> idGreaterThan(
Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterWhereClause> idLessThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
));
});
}
}
extension MessageModelQueryFilter
on QueryBuilder<MessageModel, MessageModel, QFilterCondition> {
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> dateEqualTo(
DateTime value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'date',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
dateGreaterThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'date',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> dateLessThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'date',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> dateBetween(
DateTime lower,
DateTime upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'date',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> idEqualTo(
Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> isMineEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isMine',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'message',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'message',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'message',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'message',
value: '',
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
messageIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'message',
value: '',
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
pointIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'point',
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
pointIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'point',
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> pointEqualTo(
int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'point',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
pointGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'point',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> pointLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'point',
value: value,
));
});
}
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> pointBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'point',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
}
extension MessageModelQueryObject
on QueryBuilder<MessageModel, MessageModel, QFilterCondition> {}
extension MessageModelQueryLinks
on QueryBuilder<MessageModel, MessageModel, QFilterCondition> {}
extension MessageModelQuerySortBy
on QueryBuilder<MessageModel, MessageModel, QSortBy> {
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByIsMine() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isMine', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByIsMineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isMine', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByMessage() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'message', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByMessageDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'message', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByPoint() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'point', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByPointDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'point', Sort.desc);
});
}
}
extension MessageModelQuerySortThenBy
on QueryBuilder<MessageModel, MessageModel, QSortThenBy> {
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsMine() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isMine', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsMineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isMine', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByMessage() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'message', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByMessageDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'message', Sort.desc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByPoint() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'point', Sort.asc);
});
}
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByPointDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'point', Sort.desc);
});
}
}
extension MessageModelQueryWhereDistinct
on QueryBuilder<MessageModel, MessageModel, QDistinct> {
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByDate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'date');
});
}
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByIsMine() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isMine');
});
}
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByMessage(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'message', caseSensitive: caseSensitive);
});
}
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByPoint() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'point');
});
}
}
extension MessageModelQueryProperty
on QueryBuilder<MessageModel, MessageModel, QQueryProperty> {
QueryBuilder<MessageModel, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<MessageModel, DateTime, QQueryOperations> dateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'date');
});
}
QueryBuilder<MessageModel, bool, QQueryOperations> isMineProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isMine');
});
}
QueryBuilder<MessageModel, String, QQueryOperations> messageProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'message');
});
}
QueryBuilder<MessageModel, int?, QQueryOperations> pointProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'point');
});
}
}
- chat_text_field.dart
import 'package:flutter/material.dart';
class ChatTextField extends StatelessWidget {
final TextEditingController controller;
final VoidCallback onSend;
final String? error;
final bool loading;
const ChatTextField({
super.key,
this.error,
this.loading = false,
required this.onSend,
required this.controller,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
cursorColor: Colors.blueAccent,
textAlignVertical: TextAlignVertical.center,
minLines: 1,
maxLines: 4,
decoration: InputDecoration(
errorText: error,
suffixIcon: IconButton(
onPressed: loading ? null : onSend,
icon: Icon(
Icons.send_outlined,
color: loading ? Colors.grey : Colors.blueAccent,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: Colors.blueAccent,
width: 2.0,
),
),
hintText: '메세지를 입력해주세요!',
),
);
}
}
- date_divider.dart
import 'package:flutter/material.dart';
class DateDivider extends StatelessWidget {
final DateTime date;
const DateDivider({
super.key,
required this.date,
});
@override
Widget build(BuildContext context) {
return Text(
'${date.year}년 ${date.month}월 ${date.day}일',
style: TextStyle(color: Colors.black54, fontSize: 12),
textAlign: TextAlign.center,
);
}
}
- logo.dart
import 'package:flutter/material.dart';
class Logo extends StatelessWidget {
final double width;
final double height;
const Logo({
super.key,
this.width = 200,
this.height = 200,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Image.asset(
// 로고 이미지
'asset/img/logo.png',
width: width,
height: height,
),
SizedBox(height: 32.0),
Text(
// 앱 설명 텍스트
'안녕! 나는 너의 친구 소울챗이야!\n나와 대화를 하며 친밀도를 높여보자!',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.italic,
),
),
],
);
}
}
- message.dart
import 'package:flutter/material.dart';
import 'package:soul_talk/component/point_notification.dart';
class Message extends StatelessWidget {
final bool alignLeft;
final String message;
final int? point;
const Message({
super.key,
this.alignLeft = true,
this.point,
required this.message,
});
@override
Widget build(BuildContext context) {
final alignment = alignLeft ? Alignment.centerLeft : Alignment.centerRight;
final bgColor = alignLeft ? Color(0xFFF4F4F4) : Colors.white;
final border = alignLeft ? Color(0xFFE7E7E7) : Colors.black12;
return Column(
children: [
Align(
alignment: alignment,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.0),
color: bgColor,
border: Border.all(
color: border,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Text(
message,
style: TextStyle(
color: Colors.black,
fontSize: 16,
),
),
),
),
),
if (point != null)
Align(
alignment: alignment,
child: PointNotification(
point: point!,
),
),
],
);
}
}
- point_notification.dart
import 'package:flutter/material.dart';
class PointNotification extends StatelessWidget {
final int point;
const PointNotification({
super.key,
required this.point,
});
@override
Widget build(BuildContext context) {
return Text(
'$point 포인트가 적립되었습니다!',
style: TextStyle(
color: Colors.blueAccent,
fontStyle: FontStyle.italic,
),
);
}
}'EDOC > Flutter 스터디 (2025-1)' 카테고리의 다른 글
| [4.1~4.3] 일정 관리 앱 만들기 ~ 서버와 연동하기 (0) | 2025.06.27 |
|---|---|
| [3.4 ~ 3.6] 디지털 주사위 ~ 영상 통화 (1) | 2025.05.21 |
| [3.1 ~ 3.3] 블로그 웹 앱 콜백 함수, 웹뷰, 네이티브 설정 ~ 만난 지 며칠 U&I 상태 관리, CupertinoDatePicker, Dialog, DateTime (0) | 2025.05.20 |
| [2.6 ~ 2.7] 기본 위젯 알아보기 ~ 앱을 만들려면 알아야 하는 그 밖의 지식 (0) | 2025.04.12 |
| [1.4 ~ 2.5] 다트 3.0 신규 문법 / 플러터 입문하기 (0) | 2025.04.05 |