├── Binary
│ ├── wadd
│ ├── wsub
│ ├── wmul
│ └── wdiv
├── Library
│ ├── libRandom
│ └── libCalculator
└── CI
이렇게 사용할 수 있다.
두고두고 사용해야지~
├── Binary
│ ├── wadd
│ ├── wsub
│ ├── wmul
│ └── wdiv
├── Library
│ ├── libRandom
│ └── libCalculator
└── CI
이렇게 사용할 수 있다.
두고두고 사용해야지~
GestureDetector를 사용하여 우클릭 이벤트 시 컨텍스트 메뉴가 나오게 할 수 있다.
우클릭 이벤트는 onSecondaryTapDown
또는 onSecondaryTapUp
인데 Up이 좋은게 다운 했다가 다른 곳에서 Up하면 취소되는거도 자연스럽게 적용된다.
먼저 overlay를 구해야 한다.
final RenderObject? renderObject =
Overlay.of(context).context.findRenderObject();
if (renderObject is RenderBox) {
final RenderBox overlay = renderObject;
...
렌더링할 object를 위젯 트리 상에서 가장 가까운 위젯을 build context를 통해 찾는다고 하네요
showMenu(
context: context,
position: RelativeRect.fromRect(
details.globalPosition & const Size(48, 48),
Offset.zero & overlay.size,
),
items: [
const PopupMenuItem(
value: 'menu1',
child: Text('메뉴 1'),
),
const PopupMenuItem(
value: 'menu2',
child: Text('메뉴 2'),
),
],
).then((value) {
// 메뉴 선택에 따른 동작
if (value != null) {
print('선택된 메뉴: $value');
}
});
showMenu 함수를 사용해서 context 메뉴를 정의할 수 있는데
postion 속성의 값으로 첫 번째 인자로 Rect rect
를 받고 두 번째 인자로는 Rect container
를 받습니다.
정확하게는 모르겠는데 Size(,)의 값을 수정하면 context menu가 제한된 곳을 얼만큼 나갈 수 있는지에 대한 값인 것 같고
item
을 통해서 메뉴 값을 정할 수 있고
then
을 통해서 눌린 버튼에 대한 이벤트를 정의할 수 있습니다. 눌린 버튼은 item 리스트에 정의된 value값을 then에 넘기기 때문에 switch case로 구분 가능
스냅샷 찍은 환경으로 돌아가는 과정에서 아래와 같은 에러 메시지가 발생했다.
The VM session was aborted.
결과 코드: E_FAIL (0X80004005)
구성 요소: SessionMachine
인터페이스: ISession {c0447716-ff5a-4795-b57a-ecd5fffa18a4}
멘붕이 왔지만 해결방법을 찾았다.
머신
> 저장된 상태 삭제
를 해주면 된다.
플러터에서 창 전환하는 방법은 여러 가지가 있다.
나는 Navigator.pushNamed가 좋다.
방법은 간단하다. onGenerateRoute 함수를 만들어서 어떤 이름이 들어왔을 때 어떤 페이지로 라우팅 해줄지 정하면 된다.
static const String main = 'main';
static const String login = 'login';
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
late final Widget page;
switch (settings.name) {
case RoutePath.login:
page = const LoginPage();
break;
case RoutePath.main:
page = const MainPage();
break;
}
return MaterialPageRoute(builder: (context) => page);
}
간단하게 이런식이다. 페이지 이름들을 정의하고 switch case를 사용하여 => page
이런식으로 라우팅을 한다.
main.dart에서 MaterialApp안에 아래와 같이 작성만 하면된다.
// 초기 페이지 설정
initialRoute: RoutePath.login,
// onGenerateRoute 설정
onGenerateRoute: RoutePath.onGenerateRoute,
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: const Duration(seconds: 1),
);
페이드 아웃을 하려면 FadeTransition을 사용할 수 있다.
그리고 transitionDuration
속성을 사용해서 페이드 아웃 시간도 정할 수 있다.
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(10.0, 0.0);
const end = Offset.zero;
const curve = Curves.ease;
final tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
final offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 500),
);
이건 슬라이드 방식이다.
begin, end에서 사용하는 Offset은 좌표를 나타낸다. bigin의 Offset x좌표 크기가 작으면 움직이는 속도도 작아짐
curve 관련해서는 아래 정리한다.
Curves.linear
: 일정한 속도로 애니메이션을 진행합니다.
Curves.ease
: 처음과 끝은 천천히, 중간은 빠르게 가속하는 가속도 곡선입니다.
Curves.easeIn
: 처음에 천천히 가속하는 가속도 곡선입니다.
Curves.easeOut
: 끝에서 천천히 감속하는 감속도 곡선입니다.
Curves.easeInOut
: 처음과 끝에서 천천히 가속하고 중간에서 천천히 감속하는 곡선입니다.
Curves.bounceIn
: 바운스 효과가 있는 곡선으로, 처음에 튀어오르는 효과가 있습니다.
Curves.bounceOut
: 바운스 효과가 있는 곡선으로, 끝에서 튀어오르는 효과가 있습니다.
Curves.elasticIn
: 탄력 효과가 있는 곡선으로, 처음에 잠깐 튕기는 효과가 있습니다.
Curves.elasticOut
: 탄력 효과가 있는 곡선으로, 끝에서 잠깐 튕기는 효과가 있습니다.
그렇게 해서 SlideTransition
를 사용해서 슬라이드 형식으로 한다.
transitionsBuilder
의 child
값에는 pageBuilder
에서 return한 page가 들어간다.
void _animateWindowAndNavigate(BuildContext context) {
const targetSize = Size(800, 800);
const duration = Duration(milliseconds: 500); // 애니메이션 지속 시간
const stepDuration = Duration(milliseconds: 10);
int steps = duration.inMilliseconds ~/ stepDuration.inMilliseconds;
double widthStep = (targetSize.width - appWindow.size.width) / steps;
double heightStep = (targetSize.height - appWindow.size.height) / steps;
Timer.periodic(stepDuration, (timer) {
appWindow.size = Size(
appWindow.size.width + widthStep,
appWindow.size.height + heightStep,
);
if (appWindow.size.width >= targetSize.width ||
appWindow.size.height >= targetSize.height) {
timer.cancel();
appWindow.size = targetSize;
// 창 크기 조정 후 페이지 전환
Navigator.pushNamed(context, RoutePath.main);
}
});
}
페이지가 변할 때 각 페이지에 맞는 크기를 위해서 처음에 페이지에 도달하면 그냥 바로 변하게 했더니 버벅거리는 것 같아서 사이즈를 키우고 화면 전환하도록 해봤다.
기존 크기에서 스텝마다 계산된 값(widthStep
,heightStep
)을 스텝의 시간(stepDuration
)마다 키우는 것이다.
Timer.periodic(stepDuration, (timer) {
appWindow.size = Size(
appWindow.size.width + widthStep,
appWindow.size.height + heightStep,
);
...
})
Timer.periodic
을 사용해서 stepDuration
마다 아래 로직을 수행하면서 if문을 통해 조건이 충족한다면 timer.cancel
즉 무한루프를 나오는 듯한 break를 거는 로직ㅇ비니다.
근데 위는 화면이 커질수록 버벅거린다. 그래서 마지막에 커지는 속도를 줄여야 하나 했다. 아래와 같은 코드가 나왔다.
void _animateWindowAndNavigate(BuildContext context) {
const targetSize = Size(800, 800);
const duration = Duration(milliseconds: 500); // 애니메이션 지속 시간
const timerDuration = Duration(milliseconds: 10);
const curve = Curves.decelerate;
int steps = duration.inMilliseconds ~/ timerDuration.inMilliseconds;
int currentStep = 0;
Timer.periodic(timerDuration, (timer) {
double fraction = (currentStep / steps);
// 'Curves.decelerate' 커브를 사용하여 변화율을 계산합니다.
double easedFraction = curve.transform(fraction);
double width = appWindow.size.width +
((targetSize.width - appWindow.size.width) * easedFraction);
double height = appWindow.size.height +
((targetSize.height - appWindow.size.height) * easedFraction);
appWindow.size = Size(width, height);
currentStep++;
if (fraction >= 1) {
timer.cancel();
appWindow.size = targetSize;
// 창 크기 조정 후 페이지 전환
Navigator.pushNamed(context, RoutePath.main);
}
});
}
위 코드는 마지막에 속도를 줄여야 하기에 Curves.decelerate
를 사용해서 커브 값을 얻었고 변화율을 계산하여 적용해봤지만
그래도 버벅거림은 해결하지 못했습니다.
static void animateWindowForMain(BuildContext context, String routePath) {
const targetSize = mainPageSize; // 정의된 main page size
const duration = Duration(milliseconds: 500); // 애니메이션 지속 시간
const curve = Curves.easeOutExpo; // 보다 부드러운 애니메이션을 위한 커브
final numFrames = duration.inMilliseconds ~/ 16; // 약 60fps
List<Size> frameSizes = List.generate(numFrames, (index) {
double progress = (index / numFrames);
double animatedValue = curve.transform(progress);
double width = appWindow.size.width +
((targetSize.width - appWindow.size.width) * animatedValue);
double height = appWindow.size.height +
((targetSize.height - appWindow.size.height) * animatedValue);
return Size(width, height);
});
int currentFrame = 0;
Timer.periodic(const Duration(milliseconds: 16), (timer) {
Size newSize = frameSizes[currentFrame];
appWindow.size = newSize;
currentFrame++;
if (currentFrame >= frameSizes.length) {
timer.cancel();
appWindow.size = targetSize;
// 창 크기 조정 후 페이지 전환
Navigator.pushNamed(context, routePath);
}
});
}
그래서 List<Size>
타입의 변수 안에 미리 크기들을 계산해서 넣어놓고 Timer.periodic
에선 리스트에 있는 값을 꺼내기만 하면 되니 더욱 깔끔해질 수 있었습니다.
flutter_localization
,intl
패키지 추가 flutter_localizations:
sdk: flutter
intl: ^0.18.1
꼭 위처럼 추가해야한다.
vs code Flutter Intl
extension 설치
pubspec.yaml파일에 아래 내용 입력
flutter_intl:
enabled: true
arb_dir: lib/util/lang/l10n
output_dir: lib/util/lang/generated
ctrl
+shift
+p
로 명령어 팔레트 열고 flutter intl initialize
그러면 lib/util/lang 밑에 l10n
과 generated
가 생김
끝
ctrl
+shift
+p
로 열고 intl: Add locale하고 ko
추가
그러면 intl_ko.arb파일 생긴다.
{
"@@locale" : "en",
"language" : "language"
}
{
"@@locale" : "ko",
"language" : "언어"
}
이런식으로 설정하면 된다.
ios/Runner/Info.plist
파일에서<key>CFBundleDevelopmentRegion</key>
<array>
<string>en</string>
<string>ko</string>
</array>
key태그 밑에 array태그 추가하기
import 'package:flutter_localizations/flutter_localizations.dart';
...
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
/// 이거 넣어야 한다.
locale: ref.watch(langProvider),
...
그리고 추가적으로 구현해야할 것이 locale값을 watch하는 provider 만들어서 MaterialApp
안에 locale
속성값에 watch
로 넣기
class Lang extends _$Lang {
Locale build() {
return LangState.en;
}
void toggleLanguage() {
state = LangState.isKo ? LangState.en : LangState.ko;
}
}
abstract class LangState {
static const Locale en = Locale('en');
static const Locale ko = Locale('ko');
static bool get isKo => Intl.getCurrentLocale() == ko.languageCode;
}
riverpod generater를 이용한 language provider 코드와 state 코드다.
typedef HelloThere = Pointer<Utf8> Function(Pointer<Utf8> str);
typedef
를 사용하면 특정 타입을 커스텁 할 수 있다.
단 문제점이 하나 있다. 함수 안에서는 사용못한다. 무조건 함수 밖에서 사용..
dll 사용하려면 ffi라는 패키지를 설치해야한다.
(dart:ffi랑은 다른 패키지다. 이게 있어야 Pointer<Utf8>
사용 가능)
final DynamicLibrary dylib = DynamicLibrary.open('hello.dll');
위처럼 dll파일 읽어서 일단 dll파일 자체를 DynamicLibrary 타입의 변수에 저장하고
(이 때 hello.dll
파일 위치는 root다.)
typedef HelloThereFunc = Pointer<Utf8> Function(Pointer<Utf8> str);
typedef HelloThere = Pointer<Utf8> Function(Pointer<Utf8> str);
편의를 위해서 2개의 함수형을 typedef로 정의합니다.
HelloThereFunc
는 C 라이브러리의 native 함수이고
HelloThere
는 Dart에서 사용하는 함수입니다.
타입이 Pointer<Utf8>
인 이유는 Dart와 C의 문자열 나타내는 방법이 달라서 어쩔 수 없다고 하네요
final helloThere =
dylib.lookupFunction<HelloThereFunc, HelloThere>('HelloThere');
이제 실질적으로 dll에 있는 함수를 import하는 방법입니다.
dll
파일에 DynamicLibrary
의 lookupFunction<C_native_func, Dart_func>
을 사용하여서 ('함수이름')
이 함수를 호출하는 것입니다.
코드로 보자면
final 변수명 = dll이름.lookupFunction<C_네이티브함수_시그니처, Dart_함수_시그니처>(호출할 함수이름);
라고 정리할 수 있습니다.
final Pointer<Utf8> nName = 'Park'.toNativeUtf8();
final Pointer<Utf8> nRst = helloThere(nName);
calloc.free(nName);
Center(
child: Text(nRst.toDartString()),
),
간단한 사용 예시 입니다.
먼저 인자값 선언을 합니다(nName). 스트링을 toNativeUtf8()
로 변환해주고
함수 호출하고
다 쓴 값은 free 시켜줍니다.
nRst도 free시켜줘야하는데 지금 시키면 프로그램이 꺼집니다. 이에 대해서는 조금 더 공부해봐야 합니다.
bitdoho_window라는 패키지를 이용한다.
void main() {
runApp(const MyApp());
doWhenWindowReady(() {
final win = appWindow; // 정의
const initialSize = Size(600, 450); // 사이즈 정의
win.minSize = initialSize; // 최소 사이즈 할당
win.size = initialSize; // 초기 사이즈 할당
win.alignment = Alignment.center; // 실행시 화면 위치
win.title = "Custom Window app"; // 창 이름
win.show(); // 이거 있어야 보여준다.
});
}
Scaffold(
// 테두리
body: WindowBorder(
color: const Color(0xFF0B4279),
width: 1,
child: const Row(
children: [
LeftSide(),
RightSide(),
],
),
),
),
WindowBorder
라는 위젯을 사용해서 테두리를 설정할 수 있다. 테두리의 두께, 색상을 설정할 수 있다.
child: Column(
children: [
WindowTitleBarBox(
child: Row(
children: [
Expanded(
child: MoveWindow(),
),
const WindowButtons(),
],
),
),
],
),
WindowTitleBarBox
를 정의하고 안에서 MoveWindow를 정의하면 해당 칸을 잡고 드래그하면 창도 드래그하도록 설정할 수 있다.
WindowButtonColors buttonColors = WindowButtonColors(
iconNormal: const Color(0xFF0B4279),
mouseOver: const Color(0xFF2A78C6),
mouseDown: const Color(0xFF0B4279),
iconMouseOver: const Color(0xFF0B4279),
iconMouseDown: const Color(0xFFCEE1FF),
);
Row(
children: [
MinimizeWindowButton(
colors: buttonColors,
),
MaximizeWindowButton(
colors: buttonColors,
),
CloseWindowButton(),
],
);
이미 정의 돼있는 이벤트 버튼을 사용할 수 있습니다. 여러 가지 색상 속성을 정의하여 커스텀할 수 있습니다.
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
위 내용을 windows\runner\main.cpp
의 최상단에 작성해야 합니다.
macos\runner\MainFlutterWindow.swift
에서
import bitsdojo_window_macos // FlutterMacOS 밑에 추가
이 코드를 import FlutterMacOs
밑에 추가하고
class MainFlutterWindow: NSWindow {
// 를 아래 코드로 변경
class MainFlutterWindow: BitsdojoWindow {
override func bitsdojo_window_configure() -> UInt {
return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
}
이 코드를
override func awakeFromNib() {
이 코드 위에 추가합니다.
리눅스를 위한 방법도 있는데 따로 정리하진 않겠습니다.
gpt의 답변을 정리합니다.
C:\Users\<User Name>\Documents
경로로 찾을 수 있습니다.C:\Users\User\AppData\Roaming\ [패키지이름]\ [프로젝트이름]
C:\Users\<User Name>\Downloads
에 위치합니다.C:\Users\<User Name>\AppData\Local\Temp
에 위치합니다.Async종류 | previous value [X] | previous value [O] |
---|---|---|
AsyncLoading | null | previous value |
AsyncData | current value | current value |
AsyncError | rethrow error | previous value |
Async종류 | previous error [X] | previous error [O] |
---|---|---|
AsyncLoading | null | previous error |
AsyncData | null | null |
AsyncError | current error | current error |
Async종류 | previous value [X] previous error [X] | previous value [X] previous error [O] | previous value [O] previous error [X] | previous value [O] previous error [O] |
---|---|---|---|---|
AsyncLoading | isLoading [O] hasValue [X] hasError [X] | isLoading [O] hasValue [O] hasError [X] | isLoading [O] hasValue [X] hasError [O] | isLoading [O] hasValue [O] hasError [O] |
AsyncData | isLoading [X] hasValue [O] hasError [X] | isLoading [X] hasValue [O] hasError [X] | isLoading [X] hasValue [O] hasError [X] | isLoading [X] hasValue [O] hasError [X] |
AsyncError | isLoading [X] hasValue [X] hasError [O] | isLoading [X] hasValue [O] hasError [O] | isLoading [X] hasValue [X] hasError [O] | isLoading [X] hasValue [O] hasError [O] |
autodispose를 사용하려면
final counterProvider = NotifierProvider.autoDispose<Counter, int>(Counter.new);
로 선언해야 하고
class Counter extends AutoDisposeNotifier<int>{}
위처럼 Notifier를 사용해야 한다.
final counterProvider = NotifierProvider.family<Counter, int, int>(Counter.new);
family를 사용하려면 family와 넘겨줄 인자값을 추가해야하고
class Counter extends FamilyNotifier<int, int> {
int build(int arg) {return arg;}
}
Notifier도 FamilyNotifier와 인자 추가와 build함수에 인자를 추가해야 한다.
위의 둘을 합치면 된다.
generator를 이용해서 notifier를 만들면 더 쉽다.
riverpodpart 만들기
riverpodclass로 만들기 (riverpodclass가 notifier다.)
이름 넣고 build 함수 타입 지정하기
나머지 구현
AutoDispose는 어노테이션에 따라 결정되고 family 여부는 build함수 인자값 존재 유무로 결정된다고 합니다.
추가 특이사항이 있으면 그 때 정리해야지.
2가지 있다고 한다.
enum
, sealed
.. 그렇다
추가로 AsyncValue
도 있다.
이곳의 API를 활용한다.
ptf
pts
fdataclass
factory 생성자에 인자값 넣기 required로 아니여도되구
fromJson
키워드로 fromJson 만들기 (이름은 class이름붙이고)
메서드들 만들기(empty,add,remove 등)
ptf
fdataclass
필요한 factory 메서드 만들기
riverpodpart
riverpod
으로 provider만들기
아래 문법 처럼
Dio dio(DioRef ref) {
return Dio(BaseOptions(baseUrl: '[Target URL]'));
}
riverpodpart
와 riverpodclass
로 초기 설정 후 이름 설정
build 함수 타입 정해주고 return 값 정해주기
여러 상태 컨트롤하는 함수 만들기(loading, error, success 등등)
흐름을 적자면 먼저 state의 status를 loading
으로 바꿔주고 Get요청
을 날린다음에 성공적으로 값을 가져오면 fromJson
해가지구 copyWith
해서 status success와 값을 가져온 값으로 변경
을 한다. 에러가 난다면 copyWith해서 status failure와 error에 e.toString
넣어줌
페이지에 진입했을 때 바로 api를 쏘는 동작을 하고 싶어서 initState에 api 통신하는 코드를 넣었다. 이 때 UI를 수정하는 동작도 포함돼있어서
FlutterError (Tried to modify a provider while the widget tree was building....
에러가 났다.
void initState() {
super.initState();
ref.read(enumActivityProvider.notifier).fetchActivity(activityTypes[0]);
}
// 이거를 아래로 수정
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
ref.read(enumActivityProvider.notifier).fetchActivity(activityTypes[0]);
});
}
Future.delayed
로 감싸면 에러가 해결된다. 이걸 사용하면 비동기적으로 실행하는듯 그래서 UI 그려지기 까지 기다려주는 것 같음.
특징은 multi class 이다.
sealed class StateClass {
const StateClass();
}
final class StateClassInit extends StateClass {
const StateClassInit();
String toString() => 'StateClass()';
}
setInit 대신에 provider에서 build함수 안에 fetch해주면 된다.
StateClass build() {
fetchActivity(types[0]);
return StateClass.init();
}
이 때도 위처럼만 해버리면 에러가난다.
future.delayed로 감싸는 것이 아닌 state에 init()을 먼저 할당해준다.
StateClass build() {
state = StateClass.init();
fetchActivity(types[0]);
return StateClass.init();
}
방식은 Notifier와 비슷하지만 안에 생성하는 메서드들은 모두 Future<void>
타입이다.
AsyncValue
가 좋은게 AsyncLoading
에서 이전값을 가지고 있고 AsyncData
에서 바뀐 next값을 가지고 있다.
AsyncError
에서도 value에는 이전값 보유한다.
Async할 때 try catch문에서 보통 try에 AsyncData, catch에선 AsyncError을 정의합니다. 이를 간소화시켜주는 방법입니다.
state = await AsyncValue.guard(() async {
await wait();
return state.value! + 1;
});
이렇게 하면 알아서 AsyncData와 AsyncError에 값을 넣어준다.
riverpodpart
riverpodAsyncClass / 작명 / build함수 타입 지정
build에 async 추가 및 family 할거면 build에 인자 추가
추가 값들 추가
FutureOr<int> build({required int init}) async {
return init;
}
riverpod annotation 사용하면 named parameter 사용할 수 있다.
AsyncValue
의 when
메서드 사용 시 skipError
속성이 있다.
이 속성은 default가 false고 true로 바꾸면 에러 발생 시 error: 로직에 처리된게 실행되는게 아닌 data가 있다면 이전 데이터를 보여주는 것이다.