728x90

 

서론

  • 카카오 로그인 통합의 중요성 및 일반적인 문제 소개
  • 이 글에서 해결할 주요 에러: "invalid android_key_hash or ios_bundle_id or web_site_url"

Java 버전 확인 및 JDK 설치

  • 필요성: 카카오 로그인 통합을 위한 첫걸음으로, 올바른 Java 개발 환경 설정이 필수
  • Java 버전 확인 방법
    • 명령어: java --version
  • JDK 설치 방법
    • Homebrew를 이용한 OpenJDK 11 설치
      • 명령어: brew install openjdk@11
  • JDK 경로 설정
    • .zshrc 파일을 통한 경로 설정
      • 명령어: echo 'export PATH="/opt/homebrew/opt/openjdk@11/bin:$PATH"' >> ~/.zshrc
      • 설정 적용: source ~/.zshrc

Debug 키 해시 생성 및 카카오 개발자 사이트에 등록

  • Android Key Hash 생성
    • 필요성: 카카오 로그인을 위한 안드로이드 앱 인증 절차
    • Key Hash 생성 명령어:
      • keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64
  • 카카오 개발자 사이트에 키 해시 등록
    • 상세한 단계별 등록 방법 설명

결론

  • 이러한 단계를 통해 카카오 로그인 통합 시 발생할 수 있는 주요 문제 해결
  • 추가적인 문제 해결 팁 및 유용한 리소스 링크 제공

참고 자료

  • 카카오 개발자 문서, JDK 설치 가이드 등
728x90

 

1. 화면 등록

import 'dart:convert';

import 'package:culture_app/detail_screen.dart';
import 'package:culture_app/home_screen.dart';
import 'package:culture_app/model/festival_model.dart';
import 'package:culture_app/onboarding_screen.dart';
import 'package:go_router/go_router.dart' show GoRoute, GoRouter;

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const OnboardingScreen(),
    ),
    GoRoute(
      path: '/home',
      builder: (context, state) =>
          const HomeScreen(title: 'Flutter Demo Home Page'),
    ),
    GoRoute(
      path: '/detail',
      builder: (context, state) {
      	// state에는 extra로 보낸 데이터가 이동된다
        FestivalModel festivalModel =
            FestivalModel.fromJson(state.extra as Map<String, dynamic>);
        return DetailScreen(
          festivalModel: festivalModel,
        );
      },
    ),
    GoRoute(
      path: '/users/:userId',
      builder: (context, state) => const UserScreen(id: state.pathParameters['userId']),
    ),
  ],
);

router에 데이터를 넘기는 것이 아니라, 특정 프로바이더를 추가로 생성한 후에 해당 프로바이더에 데이터를 갱신하는 방법도 있을 것 같다. 

 

2. 화면 이동

context.push(
  '/detail',
  extra: festivalModel.toJson(),
);

context.go(Uri(path: '/users/123', queryParameters: {'filter': 'abc'}).toString());

 

3. redirect

redirect: (BuildContext context, GoRouterState state) {
  if (AuthState.of(context).isSignedIn) {
    return '/signin';
  } else {
    return null;
  }   
},

 

4. SubRoute

 

5. context.go vs context.push

go : 현재 화면 삭제하고 이동

push : 쌓기

pop : 현재 화면 없애고 이동

 

728x90

 

1. Riverpod의 장점 특징

1.1. 안전한 컴파일

Provider를 사용할 때 처럼 더 이상 ProviderNotFoundException 예외가 발생하지 않고, 로딩 상태를 처리하는 것을 걱정하지 않아도 됩니다. Riverpod를 사용하면 코드가 컴파일되어 작동합니다.

 

1.2. 제한없는 Provider

Riverpod는 Provider에서 영감을 얻었지만 동일한 유형의 여러 Provider를 지원하는 것과 같은 주요 문제 중 일부를 해결합니다. 비동기 Provider를 기다리고 있습니다. 어디에서나 Provider를 추가할 수 있습니다.

 

1.3. Flutter에 의존하지 않습니다.

Flutter에 의존하지 않고 Provider를 생성/공유/테스트합니다. 여기에는 BuildContext 없이 Provider를 수신할 수 있는 기능이 포함됩니다.

 

2. riverpod 패키지 추가하기

dependencies:
  flutter:
    sdk: flutter
  riverpod: ^2.3.10

 

3. main() 메서드에 ProviderScrope 지정하기

앱 전체에 대한 공통 상태를 관리할 수 있게 됩니다. 앱의 최상위 레벨에서 상태를 제공하기 때문에, 앱의 어느 곳에서나 상태에 액세스하거나 상태를 변경할 수 있습니다.

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

 

4. Model 만들기 (freezed)

import 'package:freezed_annotation/freezed_annotation.dart';
part 'festival_model.freezed.dart';
part 'festival_model.g.dart';

@freezed
class FestivalModel with _$FestivalModel {
  const factory FestivalModel({
    required int? SEQ_NO,
    required int? ALL_KWRD_RANK_CO,
    required String? SRCHWRD_NM,
    required String? UPPER_CTGRY_NM,
    required String? LWPRT_CTGRY_NM,
    required int? MOBILE_SCCNT_VALUE,
    required int? PC_SCCNT_VALUE,
    required int? SCCNT_SM_VALUE,
    required int? SCCNT_DE,
    required String? imageLink,
  }) = _FestivalModel;
  // '일련번호',
  // '전체키워드순위수', v
  // '검색어명',
  // '상위카테고리명',
  // '하위카테고리명', v
  // '모바일검색량값',
  // 'PC검색량값',
  // '검색량합계값', v
  // '검색량일자'

// ALL_KWRD_RANK_CO UPPER_CTGRY_NM LWPRT_CTGRY_NM
// SCCNT_SM_VALUE

  factory FestivalModel.fromJson(Map<String, Object?> json) =>
      _$FestivalModelFromJson(json);
}

 

5. State, StateNotifierProvider, Notifier 만들기

import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'festival_provider.freezed.dart';

// 1. State 선언하기
@freezed
abstract class FestivalState with _$FestivalState {
  const factory FestivalState({
    @Default(20230711) int page,
    List<FestivalModel>? festivalList,
    @Default(true) bool isLoading,
    @Default(false) bool isLoadMoreError,
    @Default(false) bool isLoadMoreDone,
  }) = _FestivalState;

  const FestivalState._();
}


// 2. StateNotifierProvider 선언하기 - 3번의 Notifier, 1번의 State 넣어서 생성한다.
final festivalProvider =
    StateNotifierProvider<FestivalNotifier, FestivalState>((ref) {
  return FestivalNotifier();
});

// 3. Notifier 선언하기 - 1번의 State를 넣어서 생성한다.
class FestivalNotifier extends StateNotifier<FestivalState> {
  FestivalNotifier() : super(const FestivalState()) {
    _initFestival();
  }
  // 초기화
  _initFestival([int? initPage]) async {
    final page = initPage ?? state.page;
    final festivalList = await getFestivalList(page);

    if (festivalList == null) {
      state = state.copyWith(page: page, isLoading: false);
      return;
    }

    debugPrint('get festival is ${festivalList.length}');
    state = state.copyWith(
        page: page, isLoading: false, festivalList: festivalList);
  }

  // 추가로 불러오기
  loadMoreFestival() async {
    StringBuffer bf = StringBuffer();

    bf.write('try to request loading ${state.isLoading} at ${state.page - 1}');
    if (state.isLoading) {
      bf.write(' fail');
      return;
    }
    bf.write(' success');
    debugPrint(bf.toString());
    state = state.copyWith(
      isLoading: true,
      isLoadMoreDone: false,
      isLoadMoreError: false,
    );

    final festivalList = await getFestivalList(state.page - 1);

    if (festivalList == null) {
      // error
      state = state.copyWith(isLoadMoreError: true, isLoading: false);
      return;
    }

    debugPrint(
        'load more ${festivalList.length} posts at page ${state.page - 1}');
    if (festivalList.isNotEmpty) {
      // if load more return a list not empty, => increment page
      state = state.copyWith(
          page: state.page - 1,
          isLoading: false,
          isLoadMoreDone: festivalList.isEmpty,
          festivalList: [...state.festivalList!, ...festivalList]);
    } else {
      // not increment page
      state = state.copyWith(
        isLoading: false,
        isLoadMoreDone: festivalList.isEmpty,
      );
    }
  }

  // 새로고침
  Future<void> refresh() async {
    _initFestival(20230711);
  }
}

Notifier의 내부 state 변수를 활용해 FestivalState의 내부 값을 읽거나 변경할 수 있다. 변경할 때는 copyWith() 함수를 사용할 수 있다.

 

  state = state.copyWith(
    page: state.page - 1,
    isLoading: false,
    isLoadMoreDone: fetchedFestivalList.isEmpty,
    festivalList: [...state.festivalList!, ...fetchedFestivalList],
  );

리스트에 값을 추가할 때는 위와 같은 방식으로 추가할 수 있다.

 

 

6. Screen - ConsumerStatefulWidget 상속, PageState - ConsumerState 상속(ref 사용 가능)

// 1. ConsumerStatefulWidget 상속
class HomeScreen extends ConsumerStatefulWidget {
  const HomeScreen({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

// 2. ConsumerState 상속
class _MyHomePageState extends ConsumerState<HomeScreen> {
  ScrollController _controller = ScrollController();
  int oldLength = 0;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() async {
      // debugPrint('pixel is ${_controller.position.pixels}');
      // debugPrint('max is ${_controller.position.maxScrollExtent}');
      if (_controller.position.pixels >
          _controller.position.maxScrollExtent -
              MediaQuery.of(context).size.height) {
        if (oldLength == ref.read(festivalProvider).festivalList!.length) {
          // 이전 데이터 로딩이 완료되었는지 확인하는 로직.
          // 이전 loadMoreFestival() 호출로 인해 데이터가 이미 업데이트되었다면
          // 새로운 loadMoreFestival() 호출을 하지 않도록 합니다.
          // 이는 불필요한 API 호출이나 연산을 줄이는 데 도움이 됩니다.
          ref.read(festivalProvider.notifier).loadMoreFestival();
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
  	// festivalProvider가 사용한 FestivalState를 읽어옵니다.
    // state.isLoading, state.festivalList와 같은 형태로 값을 읽을 수 있다.
    final state = ref.watch(festivalProvider);

    return Scaffold(
      body: RefreshIndicator(
        onRefresh: () {
          // notifier 가져오기 - 새로고침 
          return ref.read(festivalProvider.notifier).refresh();
        },
        child: festivalList == null || festivalList.isEmpty ? 
            const Center(
              child: Text('error'),
            );
          : ListView.builder(
            padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
            controller: _controller,
            itemCount: festivalList.length + 1,
            itemBuilder: (ctx, index) {
              if (index == state.festivalList.length) {
                if (state.isLoadMoreError) {
                  return const Center(
                    child: Text('에러가 발생했습니다!'),
                  );
                }
                if (isLoadMoreDone) {
                  return const Center(
                    child: Text(
                      '조회가 완료되었습니다!',
                      style: TextStyle(color: Colors.green, fontSize: 20),
                    ),
                  );
                }
                return const LinearProgressIndicator();
              }
              return FestivalListItem(
                index: index,
                festivalModel: festivalList[index],
              );
            }),
      ),
    );
  }
}

 

7. Notifier의 함수 호출

ref
.read(onboardingIndexProvider.notifier)
.changePageIndex(index);
728x90

 

0. pubspec.yaml 에 기초 프로젝트 설정하기

name: culture_app
description: A new Flutter project.
publish_to: 'none' 
version: 1.0.0+1
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_screenutil: ^5.8.4
  url_launcher: ^6.1.12
  cached_network_image: ^3.2.3
  flutter_dotenv: ^5.1.0
  font_awesome_flutter: ^10.5.0
  dio: ^5.2.1+1
  awesome_dialog: ^2.2.1
  shimmer: ^3.0.0
  intl: any
  flutter_riverpod: ^2.3.6
  riverpod: ^2.3.6  
  go_router: ^9.0.3
  connectivity_plus: ^4.0.1
  pretty_dio_logger: ^1.3.1
  riverpod_annotation: ^2.1.1
  shared_preferences: ^2.2.0
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1
  lottie: ^2.4.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  flutter_localizations:
    sdk: flutter
  freezed: ^2.4.1
  build_runner: ^2.4.6
  flutter_launcher_icons: ^0.13.1
  json_serializable: ^6.7.1

flutter:
  uses-material-design: true
  fonts:  
    - family: 'NotoSansKR'
      fonts:  
        - asset: assets/fonts/NotoSansKR/NotoSansKR-Regular.otf
        - asset: assets/fonts/NotoSansKR/NotoSansKR-Medium.otf
          weight: 500
        - asset: assets/fonts/NotoSansKR/NotoSansKR-Bold.otf
          weight: 700
        - asset: assets/fonts/NotoSansKR/NotoSansKR-Black.otf
          weight: 900
  assets:
    - assets/images/
    - assets/lotties/
    # - assets/images/common/
    # - assets/images/icon/
    # - assets/config/
    - assets/fonts/NotoSansKR/

 

1. main에 ProviderScope 설정하기

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

 

2. GoRouter로 화면 지정하기

import 'dart:convert';

import 'package:culture_app/detail_screen.dart';
import 'package:culture_app/home_screen.dart';
import 'package:culture_app/model/festival_model.dart';
import 'package:culture_app/onboarding_screen.dart';
import 'package:go_router/go_router.dart' show GoRoute, GoRouter;

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const OnboardingScreen(),
    ),
    GoRoute(
      path: '/home',
      builder: (context, state) =>
          const HomeScreen(title: 'Flutter Demo Home Page'),
    ),
    GoRoute(
      path: '/detail',
      builder: (context, state) {
        FestivalModel festivalModel =
            FestivalModel.fromJson(state.extra as Map<String, dynamic>);
        return DetailScreen(
          festivalModel: festivalModel,
        );
      },
    ),
  ],
);

 

3. http 클라이언트 코드 작성하기

import 'dart:convert';

import 'package:culture_app/model/festival_model.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<List<FestivalModel>> getFestivalList(int year) async {
  debugPrint('httpClient loading page $year');
  // year = 20230711;
  try {
    final response =
        await http.get(Uri.parse('http://3.38.247.254:3060/festivals/$year'));
    print('year');
    print(year);
    // print response
    print('response.body');
    print(response.body);
    final List<FestivalModel> festivalList =
        (jsonDecode(response.body)['data'] as List)
            .map((e) => FestivalModel.fromJson(e))
            .toList();
    return festivalList;
  } catch (ex, st) {
    debugPrint(ex.toString());
    debugPrint(st.toString());
    return [];
  }
}

 

4. Riverpod :: 축제 리스트 페이징을 위한 state, provider, notifier 선언

import 'dart:async';

import 'package:culture_app/model/festival_model.dart';
import 'package:culture_app/http_client.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'festival_provider.freezed.dart';

// state, provider, notifier

// festival state
@freezed
abstract class FestivalState with _$FestivalState {
  const factory FestivalState({
    @Default(20230711) int page,
    List<FestivalModel>? festivalList,
    @Default(true) bool isLoading,
    @Default(false) bool isLoadMoreError,
    @Default(false) bool isLoadMoreDone,
  }) = _FestivalState;

  const FestivalState._();
}

// festival provider
final festivalProvider =
    StateNotifierProvider<FestivalNotifier, FestivalState>((ref) {
  return FestivalNotifier();
});

// festival notifier
class FestivalNotifier extends StateNotifier<FestivalState> {
  FestivalNotifier() : super(const FestivalState()) {
    _initFestival();
  }
  // 초기화
  _initFestival([int? initPage]) async {
    final page = initPage ?? state.page;
    final festivalList = await getFestivalList(page);

    if (festivalList == null) {
      state = state.copyWith(page: page, isLoading: false);
      return;
    }

    debugPrint('get festival is ${festivalList.length}');
    state = state.copyWith(
        page: page, isLoading: false, festivalList: festivalList);
  }

  // 추가로 불러오기 (페이징)
  loadMoreFestival() async {
    StringBuffer bf = StringBuffer();

    bf.write('try to request loading ${state.isLoading} at ${state.page - 1}');
    if (state.isLoading) {
      bf.write(' fail');
      return;
    }
    bf.write(' success');
    debugPrint(bf.toString());
    state = state.copyWith(
      isLoading: true,
      isLoadMoreDone: false,
      isLoadMoreError: false,
    );

    final festivalList = await getFestivalList(state.page - 1);

    if (festivalList == null) {
      // error
      state = state.copyWith(isLoadMoreError: true, isLoading: false);
      return;
    }

    debugPrint(
        'load more ${festivalList.length} posts at page ${state.page - 1}');
    if (festivalList.isNotEmpty) {
      // if load more return a list not empty, => increment page
      state = state.copyWith(
          page: state.page - 1,
          isLoading: false,
          isLoadMoreDone: festivalList.isEmpty,
          festivalList: [...state.festivalList!, ...festivalList]);
    } else {
      // not increment page
      state = state.copyWith(
        isLoading: false,
        isLoadMoreDone: festivalList.isEmpty,
      );
    }
  }

  // 새로고침
  Future<void> refresh() async {
    _initFestival(20230711);
  }
}

 

5. Riverpod :: 인덱스 값 관리를 위한 state, provider, notifier 선언

import 'dart:async';

import 'package:culture_app/model/festival_model.dart';
import 'package:culture_app/http_client.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'onboarding_index_provider.freezed.dart';

@freezed
abstract class OnboardingIndexState with _$OnboardingIndexState {
  const factory OnboardingIndexState({
    @Default(0) int pageIndex,
  }) = _OnboardingIndexState;
  const OnboardingIndexState._();
}

final onboardingIndexProvider =
    StateNotifierProvider<OnboardingIndexNotifier, OnboardingIndexState>((ref) {
  return OnboardingIndexNotifier();
});

class OnboardingIndexNotifier extends StateNotifier<OnboardingIndexState> {
  OnboardingIndexNotifier() : super(OnboardingIndexState()) {
    _initFestival();
  }
  // 초기화
  _initFestival() async {}

  // pageIndex를 index 파라미터로 변경하는 함수
  void changePageIndex(int index) {
    state = state.copyWith(pageIndex: index);
  }
}

 

6. freezed 빌드를 위한 freezed.sh 쉘 스크립트

flutter pub run build_runner build --delete-conflicting-outputs

 

 

728x90

 

아래와 같이 자주 쓰는 색을 변수로 저장하여, 재사용성이 높아져 코드의 유지보수성이 증가한다.

import 'package:flutter/material.dart';

// Basic Color
const Color kWhiteColor = Color(0xFFFFFFFF);
const Color kBlackColor = Color(0xFF000000);

// System Color
const Color kSystemErrorColor = Color(0xFFFC444F);
const Color kSystemSuccessColor = Color(0xFF30E181);
const Color kSystemWarningColor = Color(0xFFFFD362);

// Color Pallette
const Color kPrimaryColor = Color(0xFF4880EE);
Color kPrimaryColorOp = const Color(0xFF4880EE).withOpacity(0.35);

const Color kIconBackgroundColor = Color(0xFFFAFAFC);
const Color kKakaoBackgroundColor = Color(0xFFFEE500);
const Color kFacebookBackgroundColor = Color(0xFF1E70E9);

// 텍스트 폼
const Color kTextLeftBarColor = Color(0xFF4B6AEA);
const Color kTextBottomBarColor = Color(0xFF5991F4);

const Color kGrayF2 = Color(0xFFF2F3F5);
const Color kGrayF5 = Color(0xFFF5F5F5);
const Color kGrayF8 = Color(0xFFF8F8FA);
const Color kGrayAF = Color(0xFFAFB7BF);
const Color kGray87 = Color(0xFF878B95);

const Color kBlue37 = Color(0xFF3767D3);

// nav
const Color kNavBackground = Color(0xFF1F1F1F);

const Color kPapsText = Color(0xFF3F4656);
728x90

여러 텍스트 스타일을 미리 저장해두고, 

import 'package:culture_app/constant_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

// ...을 알려주세요
TextStyle kGrade1 = TextStyle(
  fontSize: 30.sp,
  color: kBlackColor,
  fontWeight: FontWeight.w700,
  height: 1.2,
);

// ...을 알려주세요
TextStyle kHeadline1 = TextStyle(
  fontSize: 24.sp,
  color: kBlackColor,
  fontWeight: FontWeight.w700,
  height: 1.5,
);

//
TextStyle kHeadline2 = TextStyle(
  fontSize: 22.sp,
  color: kBlackColor,
  fontWeight: FontWeight.w500,
  height: 1.5,
);

TextStyle kHeadline3 = TextStyle(
  fontSize: 20.sp,
  fontWeight: FontWeight.w500,
  height: 2,
);

//
TextStyle kHeadline4 = TextStyle(
  fontSize: 18.sp,
  fontWeight: FontWeight.w500,
  height: 2,
);

// 로그인을 하면 , 아이콘 텍스트
TextStyle kBody1 = TextStyle(
  fontSize: 16.sp,
  color: kBlackColor,
  fontWeight: FontWeight.w500,
  height: 1.5,
);
// 카카오계정으로 로그인 ...
TextStyle kBody2 = TextStyle(
  fontSize: 14.sp,
  fontWeight: FontWeight.w700,
  height: 1.5,
);

// 텍스트 버튼
TextStyle kBody3 = TextStyle(
  fontSize: 14.sp,
  fontWeight: FontWeight.w500,
  height: 1.5,
);
// 텍스트 버튼
TextStyle kBody4 = TextStyle(
  fontSize: 12.sp,
  fontWeight: FontWeight.w500,
  height: 1.5,
);
TextStyle kBody5 = TextStyle(
  fontSize: 10.sp,
  fontWeight: FontWeight.w500,
  height: 1.5,
);
TextStyle kBody6 = TextStyle(
  fontSize: 8.sp,
  fontWeight: FontWeight.w700,
  height: 1.0,
);

// 폼 텍스트
// 힌트
TextStyle kHint1 = TextStyle(
  fontSize: 20.sp,
  fontWeight: FontWeight.w500,
  color: const Color(0xffAFB7BF),
  height: 1.8,
);
// 힌트
TextStyle kHint2 = TextStyle(
  fontSize: 14.sp,
  color: const Color(0xffAFB7BF),
  height: 1.8,
);
// 라벨
TextStyle kLabel1 = TextStyle(
  fontSize: 12.sp,
  fontWeight: FontWeight.w500,
  color: const Color(0xff8A929E),
);
// 텍스트
TextStyle kText1 = TextStyle(
  fontSize: 20.sp,
  fontWeight: FontWeight.w500,
  color: kBlackColor,
);

// 에러
TextStyle kError1 = TextStyle(
  fontSize: 12.sp,
  fontWeight: FontWeight.w500,
  color: kSystemErrorColor,
);

// 로그인을 하면 , 아이콘 텍스트
TextStyle kNextButton1 = TextStyle(
  fontSize: 16.sp,
  fontWeight: FontWeight.w400,
  height: 1.5,
);

 

아래와 같이 copyWith() 함수를 사용해 변경할 부분만 변경하면 텍스트 스타일을 쉽게 사용 가능하다.

Text(
    '${widget.festivalModel!.SRCHWRD_NM}\n영상으로\n체험하기',
    textAlign: TextAlign.center,
    style: kBody1.copyWith(
      height: 1.0,
      // color: kPapsText,
    ),
  ),

 

728x90

 

1. 배경

데이터베이스에서 데이터를 가져와 앱 프론트에 표시할 때, 서버와 API통신하는 경우가 많은데, API 통신 코드를 모듈화하여 유지보수가 용이하게 할 필요성이 있었다. 다양한 기능을 가진 Interceptor를 옵션에 추가해 개발 시 디버깅이 편하게 해야 했다.

 

2. 요구사항

2.1. dio

https://pub.dev/packages/dio

 

dio | Dart Package

A powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, File Downloading, Timeout etc.

pub.dev

인기있는 http 라이브러리다. 다양한 기능이 포함되어있다.

 

2.2. alice

https://pub.dev/packages/alice

 

alice | Flutter Package

Alice is an HTTP Inspector tool which helps debugging http requests. It catches and stores http requests and responses, which can be viewed via simple UI.

pub.dev

api 통신을 디버깅하거나 receiveTimeout 등의 설정을 할 수 있다.

 

2.3. hive

https://pub.dev/packages/hive

 

hive | Dart Package

Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.

pub.dev

로그인 시 jwt token을 hive에 저장하고, API 통신 시 헤더에 이를 추가하여 유저임을 인증할 수 있다.

 

2. 코드

import 'dart:async';
import 'package:alice/alice.dart';
import 'package:hive/hive.dart';
import 'package:papsm/app/data/environment/environment.dart';
import 'package:papsm/app/dio/constant_dio.dart';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/material.dart';

import 'package:papsm/app/dio/interceptor/logging_interceptor.dart';

class DioHelper {
  Dio? dio;
  Alice? alice;

  DioHelper() {
    alice = Alice(showNotification: false);
    final options = BaseOptions(
      receiveTimeout: ConstantDio.timeout,
      connectTimeout: ConstantDio.timeout,
    );

    options.baseUrl = EnvironmentConstant.config ?? 'test';
    dio = Dio(
      options,
    );

    dio!.interceptors.add(LoggingInterceptors());
    dio!.interceptors.add(alice!.getDioInterceptor());
    dio!.interceptors.add(DioCacheInterceptor(options: dioCacheOptions));
  }

  Future<Map<String, dynamic>> get(String url,
      {dynamic param, bool? refresh}) async {
    final userModelBox = await Hive.openBox('userModelBox');
    String? token = userModelBox.get('token');
    dio!.options.headers["authorization"] = 'Bearer $token';
    final response = await dio!.get(
      url,
      queryParameters: param,
      options: refresh == true
          ? dioCacheOptions.copyWith(policy: CachePolicy.refresh).toOptions()
          : null,
    );
    return response.data as Map<String, dynamic>;
  }

  Future<Map<String, dynamic>> post(String url, dynamic body) async {
    final userModelBox = await Hive.openBox('userModelBox');
    String? token = userModelBox.get('token');
    dio!.options.headers["authorization"] = 'Bearer $token';
    final response = await dio!.post(url, data: body);
    return response.data as Map<String, dynamic>;
  }

  Future<Map<String, dynamic>> patch(String url, dynamic body) async {
    final userModelBox = await Hive.openBox('userModelBox');
    String? token = userModelBox.get('token');
    dio!.options.headers["authorization"] = 'Bearer $token';
    final response = await dio!.patch(url, data: body);
    debugPrint('[Response] ${response.data.runtimeType} ${response.data}');
    return response.data as Map<String, dynamic>;
  }

  Future<Map<String, dynamic>> put(String url, dynamic body) async {
    final userModelBox = await Hive.openBox('userModelBox');
    String? token = userModelBox.get('token');
    dio!.options.headers["authorization"] = 'Bearer $token';
    final response = await dio!.put(url, data: body);
    debugPrint('[Response] ${response.data.runtimeType} ${response.data}');
    return response.data as Map<String, dynamic>;
  }

  Future<Map<String, dynamic>> delete(String url, dynamic body) async {
    final userModelBox = await Hive.openBox('userModelBox');
    String? token = userModelBox.get('token');
    dio!.options.headers["authorization"] = 'Bearer $token';
    final response = await dio!.delete(url, data: body);
    debugPrint('[Response] ${response.data.runtimeType} ${response.data}');
    return response.data as Map<String, dynamic>;
  }

  void errorCheck(Map<String, dynamic> result, String err) {
    if (result['code'] != 200) {
      throw err;
    }
  }
}

CacheOptions dioCacheOptions = CacheOptions(
  store: MemCacheStore(),
  hitCacheOnErrorExcept: [401, 403],
  maxStale: const Duration(hours: 6),
);

DioHelper 생성자에서 .env 파일에서 저장된 서버의 주소로 baseUrl을 설정하였다.

dio interceptor로 LoggingInterceptor를 추가하여 API 통신 시 보내는 데이터와 받는 데이터를 로깅하는 기능을 추가했다.

dio interceptor로 CacheInterceptor를 추가하여 동일한 API 통신 시 6시간 마다 MemCacheStore에 저장하여 재활용할 수 있도록 하였다. (새로 요청하려면 refresh option을 true로 두면 된다.)

hitCacheOnErrorExcept에서 401, 403 error가 발생했을 때는 다시 요청하도록 했다.

 

728x90

1. 배경

매번 하는 일인데, 배포 심사할 때만 하다보니 까먹는 경우가 잦아 정리하려고 한다

2. 의존성 추가하기

flutter_launcher_icons: "^0.11.0"

https://pub.dev/packages/flutter_launcher_icons

 

flutter_launcher_icons | Dart Package

A package which simplifies the task of updating your Flutter app's launcher icon.

pub.dev

 

3. flutter_launcher_icons.yaml

루트 폴더에 앱 아이콘의 경로 및 기타 정보를 설정하는 yaml 파일을 생성한다.

flutter_icons:
  android: "launcher_icon"
  ios: true
  remove_alpha_ios: true
  image_path: "assets/images/app_icon.png"

 

4. 앱 아이콘 적용하기

터미널에 아래 명령어를 사용하면, 해상도별로 android, ios 앱 아이콘이 적용된다

flutter pub run flutter_launcher_icons -f flutter_launcher_icons.yaml

 

 

728x90

Flutter 츠누봇 [Pull Request] :: [학식] 페이지 UI 퍼블리싱

https://github.com/cnu-bot/cnubot-client-app/pull/22

 

[학식] 페이지 UI 퍼블리싱 by CheolheeLee0 · Pull Request #22 · cnu-bot/cnubot-client-app

Describe your changes 영상으로 확인하기 학식 페이지 UI 개발 1. 공통 컴포넌트 설계 cafeteria_button.dart (1, 2, 3, 4학, 긱사, 생과대 식당 선택 버튼) cafeteria_listview.dart (1, 2, 3, 4학, 긱사, 생과대 식당 선택 L

github.com

 

1. 요구사항

- 디자인 참고하여 UI 개발

- 공통 컴포넌트 설계

- TextStyle, Color 생성

- 1, 2, 3, 4학, 긱사, 생과대 식단 가져오기

- 원산지 파싱

 

2. 작업내용

영상으로 확인하기

https://www.youtube.com/shorts/EcfJ5q3RGHA

 

 

작업 상세내용

 

3. 코드리뷰

- conflic 해결방법 : git merge 제안

- 파일명, 함수명 오타 수정

 

4. Merge

 

728x90

Flutter 츠누봇 [Pull Request] :: [소식] 페이지 API 연동

https://github.com/cnu-bot/cnubot-client-app/pull/20

 

[소식] 페이지 API 연동 by CheolheeLee0 · Pull Request #20 · cnu-bot/cnubot-client-app

Describe your changes #18 영상으로 확인하기 디자인 변경사항 적용 상단 Logo 상단 Home Button bottom navigation bar svg icon, color NoticeModel 필드 수정 nullable : boardNum, picUrl, period API에 맞추어 필드 추가 백마광장

github.com

 

1. 요구사항

 

 

2. 작업내용

영상으로 확인하기

https://www.youtube.com/watch?v=1Xn7CVbj8jU 

 

작업 상세내용

3. 코드리뷰

- Shell script 사용

- Get.back()을 Get.toNamed()로 수정

- enum에 API요청을 위한 param 변수 추가

 

4. Merge

 

 

 

728x90

Flutter 츠누봇 [Pull Request] :: [소식] 페이지 UI 퍼블리싱

https://github.com/cnu-bot/cnubot-client-app/issues/11

 

[소식] 페이지 UI 퍼블리싱 · Issue #11 · cnu-bot/cnubot-client-app

Feature Description 소식 페이지 퍼블리싱을 진행합니다. 아직 백엔드 API작업이 완료되지 않아서 더미데이터로 작업하셔야할 것 같습니다. UI만 작업하면 됩니다. Todo list 소식 페이지 UI작업 백마광

github.com

 

1. 요구사항

- 아래 디자인을 바탕으로 UI 개발

- 데이터는 임시로 더미데이터를 생성하여 진행

 

2. 작업내용

영상으로 확인하기

https://www.youtube.com/shorts/y4_hiaOdu1o

 

작업 상세내용

3. 코드리뷰

- default로 정해져있는 코드 생략

- 유틸함수 분리

- Color 변수 이름 컨벤션 : kBlue213B85 -> kBlue21 로 수정

- Color 변수는 copyWith() 함수를 사용하여 재사용성 높임

- BottomNavigation 추가

 

 

 

4. Merge

 

 

 

 

 

728x90

Flutter 츠누봇 [Pull Request] :: flutter_lints rules 추가

https://github.com/cnu-bot/cnubot-client-app/pull/17

 

feat: flutter_lints rules 추가 by CheolheeLee0 · Pull Request #17 · cnu-bot/cnubot-client-app

Describe your changes flutter lints에 여러 옵션이 있어서 필요할 것 같은 부분 추가해두었습니다! linter: rules: avoid_print: true // release모드에서 출력 안되게 변경 prefer_single_quotes: true // single quote 통일 requir

github.com

 

1. 요구사항

- 협업 시 코드의 일관성을 위해 lint rule 적용

 

2. 작업내용

root폴더의 analysis_options.yaml 파일에 해당 lint 규칙 추가

linter:
  rules:
    avoid_print: true		// release모드에서 출력 안되게 변경
    prefer_single_quotes: true		// single quote 통일
    require_trailing_comma: true
    always_use_package_imports: true	// 절대경로 사용
    prefer_final_locals: true		// final 사용 (컴파일러 성능 향상)
    camel_case_types: true		// camel_case_types

 

lint rule 적용하면서 생긴 Problems 수정

 

3. 코드리뷰

 

4. Merge

728x90

Flutter 츠누봇 [Pull Request] :: clean & 3-layered architecture 적용

https://github.com/cnu-bot/cnubot-client-app/pull/9

 

refactor: apply clean & 3-layered architecture by CheolheeLee0 · Pull Request #9 · cnu-bot/cnubot-client-app

Describe your changes refactor: apply clean & 3-layered architecture docs: Add Architecture info, freezed command API 연동 테스트 완료했습니다 dioHelper - provider - repository - controller - GetView ...

github.com

 

1. 요구사항

- Project resetting

-folder structure refactoring

- http => dio

- color palette

- font typography


2. 작업 내용

README.md 파일 업데이트

clean & 3-layered 아키텍쳐 적용

dioHelper - provider - repository - controller - GetView 순서로 API에서 가져온 데이터 표시

 

.env 파일 추가

env 파일은 API서버 IP, Service key 같은 민감한 정보를 저장합니다. (.gitignore에 .env추가 완료)

 
 


3. 코드리뷰

- 폴더 오타 수정

- ios platform 버전 12.0으로 수정 (Podfile)

- 파일명 단수로 통일

- 절대경로로 통일

- 필요없는 코드 제거

- dioHelper 추가

- http 의존성 제거 (dio로 대체)

- DI (의존성 주입) 

- CI 오류 수정



4. Merge

728x90

 Flutter 츠누봇 [Pull Request] :: 의존성 추가, 화면 비율 설정

https://github.com/cnu-bot/cnubot-client-app/pull/3

 

Add dependencies, Add Pull Request Template and Set the reference design size ratio to iPhone 12 mini for easy UI development. (

Description #1 #2 #4 Fixes #1 #2 #4 Type of change New feature (non-breaking change which adds functionality) This change requires a documentation update Checklist: My code follows the s...

github.com

 

1. 요구사항

- 의존성 추가

- 화면 비율 설정

 

2. 작업 내용

의존성 추가

cupertino_icons: ^1.0.2
freezed_annotation: ^2.1.0
json_annotation: ^4.6.0
http: ^0.13.5
get: ^4.6.5
url_launcher: ^6.1.5
cached_network_image: ^3.2.1
shimmer: ^2.0.0
intl: ^0.17.0
device_info_plus: ^3.2.4
flutter_screenutil: ^5.5.3+2
flutter_local_notifications: ^9.7.0
flutter_native_timezone: ^2.0.0
carousel_slider: ^4.1.1
url_launcher: ^6.1.5
flutter_dotenv: ^5.0.2
hive: ^2.2.3
hive_flutter: ^1.1.0
flutter_launcher_icons: ^0.10.0

 

화면 비율 설정 (375, 812)

return ScreenUtilInit(
      designSize: const Size(375, 812),
      minTextAdapt: true,
      builder: (context, child) {
        return GetMaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'cnubot',
          initialRoute: Routes.HOME,
          initialBinding: AppBinding(),
          getPages: AppPages.routes,
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
        );
      },
    );

 

3. 코드리뷰

- 불필요한 용량을 차지하기 때문에 현재 필요없는 의존성(dependencies)들 제거하기

 

4. Merge

 

 

728x90

https://youtube.com/shorts/u_hzF982xbI?feature=share 

배경

초중고 학생들을위한 체력측정 앱 개발 중에, 키보드 위에 확인 버튼위젯을 고정할 일이 생겼는데 이슈들이 발생해서 정리하고자 한다.

 

요구사항

1. 키보드 바로 위에 '확인' 버튼이 있어야 함

2. 키보드에 입력 데이터가 가려지면 안됨

3. 버튼을 선택할 때 키보드가 없어져야 함

 

해결방법

double bottomViewInsets = MediaQuery.of(context).viewInsets.bottom;
bool keyboardOn = bottomViewInsets != 0;	// 키보드 존재 여부 판단

키보드가 올라가면 키보드의 높이만큼 bottomViewInsets이 증가하는 점을 이용해 키보드가 올라왔는지 파악할 수 있다.

 

  keyboardOn
      ? Align(
          alignment: Alignment.bottomCenter,
          child: InkWell(
            onTap: () async {
              int index = controller.registerStateIndex.value;
              FocusNode focusNode = controller.focusNodeList[index];
              if (focusNode.hasFocus) {
                controller.nextStep();
                Future.delayed(const Duration(milliseconds: 100), () {
                  int index = controller.registerStateIndex.value;
                  FocusNode focusNode = controller.focusNodeList[index];
                  FocusScope.of(context).requestFocus(focusNode);
                });
              } else {
                FocusScope.of(context).unfocus();
              }
            },
            child: Container(
              width: 375.w,
              height: 56.h,
              decoration: const BoxDecoration(
                color: kPrimaryColor,
              ),
              child: Center(
                child: Text(
                  '확인',
                  style: kNextButton1.copyWith(color: kWhiteColor),
                ),
              ),
            ),
          ),
        )
      : const SizedBox.shrink(),

키보드가 올라왔을 때만 "확인" 버튼을 보여주면 되므로, keyboardOn 변수로 분기처리를 해준다.

여기서 다음 입력할 필드가 텍스트임에도 불구하고 키보드가 내려가는 문제가 있었는데,

원인은 controller.nextStep(); 시 포커싱할 포커스 노드의 index의 값이 1이 증가하는데, UI가 rebuild 되므로 아래코드의 index에 반영되기 까지 시간이 걸려서 Delay를 100ms 주어서 해결했다.

 

Align(
  alignment: Alignment.bottomCenter,
  child:
)
Alignment.bottomCenter 를 사용하면 맨 아래에 위젯이 배치되므로, CustomScrollView 바깥에 위젯을 배치했다. 

Next To do

학교정보 검색, 선택 화면 개발

이메일 인증, 비밀번호 입력 화면 개발

728x90

Firestore 구조

 

CollectionReference

 

DocumentReference

 

 

 

728x90

 

 

1. 채팅 기능


1.1. 채팅 어플

 

https://www.youtube.com/watch?v=ot5ctOZe2X0&ab_channel=DynamicCoding 

 

1.2. 채팅 백엔드

 

https://www.youtube.com/watch?v=I6UCPL1svOA&ab_channel=DynamicCoding 

 

1.3. 추가할 기능

채팅 UI 개선

말풍선 크기를 채팅 메시지와 맞게 수정

메시지가 1개 이상인 채팅방만 보이도록 수정

카카오 로그인 uid 사용

대화 상대방 검색 시 부분 문자열로도 검색 가능하도록 수정

 

2. 카카오 로그인


2.1. pub.dev 라이브러리

 

https://pub.dev/packages/kakao_flutter_sdk

 

kakao_flutter_sdk | Flutter Package

A flutter plugin for Kakao API, which supports Kakao login, KakaoLink, User API, KakaoTalk API, KakaoStory API, and Push API.

pub.dev

 

2.2. kakao developers 등록

 

 

3. 웹발신 문자인증


Firebase를 사용하여 문자인증 구현

 

https://100sucoding.tistory.com/44

 

플러터(flutter) 파이어베이스 Auth 로 휴대폰(SMS) 본인인증 무료로 간단히 하기(꼼수)

초기 발행 : 2021년 6월 실습 환경 : 맥북 / BigSur / Intel 앱 회원가입 부분 중에 특히나 휴대폰 인증이 필요한 앱이 있죠. 가령 쇼핑 앱 같은 거요. 그럴 때 휴대폰 인증 코드를 보내고 그걸 받아

100sucoding.tistory.com

 

4. 결제 모듈


4.1. 아임포트

 

https://pub.dev/packages/iamport_flutter/score

 

iamport_flutter | Flutter Package

Plugin that allows Flutter to use Iamport payment and certification functions.

pub.dev

 

 

4.2. 부트페이

 

https://docs.bootpay.co.kr/online/pg?language=flutter 

 

 

728x90

1. Flutter Devtools  개념 이해 

 

1) Flutter devtools 설치

Flutter devtools 사용 사진

 

Flutter devtools의 기능

  • Flutter 앱의 UI 레이아웃과 상태를 검사
  • Flutter 앱에서 UI 버벅거림 성능 문제를 진단
  • Flutter 또는 Dart 명령줄 앱의 소스 수준 디버깅
  • Flutter 또는 Dart 명령줄 앱에서 메모리 문제를 디버그
  • 실행 중인 Flutter 또는 Dart 명령줄 앱에 대한 일반 로그 및 진단 정보 확인

 

Flutter devtools 설치 (VS code)

VS Code 명령 팔레트에서 Dart: Open DevTools 명령 입력

Terminal에서 devtools 설치

flutter pub global activate devtools

 

DevTools application server 실행하기

flutter pub global run devtools

 

2) Flutter 인스펙터 사용하기

인스펙터는 Flutter 위젯 트리를 시각화하고 탐색하는 데 사용됩니다.

  • 기존 레이아웃 이해
  • 레이아웃 문제 진단

Flutter 인스펙터 기능

위젯 모드 선택

장치에서 검사할 위젯을 선택하려면 이 버튼을 활성화하십시오.

위젯 검사

 

트리 새로 고침 

현재 위젯 정보를 다시 로드합니다.

 

성능 오버레이 

GPU 및 CPU 스레드에 대한 성능 그래프 표시를 토글합니다. 이 그래프 해석에 대한 자세한 내용은 Flutter 성능 프로파일링의 성능 오버레이 를 참조하세요 .

 

iOS 

Android와 iOS 간에 렌더링 및 제스처 동작을 전환합니다.

 

디버그 페인트 

테두리, 패딩, 정렬 및 스페이서를 표시하는 렌더링에 시각적 디버깅 힌트를 추가합니다.

 

페인트 베이스라인 

각 RenderBox가 각 텍스트 기준선에서 선을 그리도록 합니다.

 

느린 애니메이션 

애니메이션을 느리게 하여 육안 검사를 가능하게 합니다.

 

무지개 다시 칠하기 

다시 칠할 때 레이어에 회전하는 색상을 표시합니다.

 

디버그 모드 배너 

디버그 빌드를 실행할 때도 디버그 배너 표시를 토글합니다.

 

Inspecting a widget

대화형 위젯 트리를 탐색하여 주변 위젯을 보고 해당 필드 값을 볼 수 있습니다.

 

위젯 트리에서 개별 UI 요소를 찾으려면 도구 모음에서 위젯 모드 선택 버튼을 클릭합니다 . 이렇게 하면 기기의 앱이 "위젯 선택" 모드로 전환됩니다. 앱 UI에서 위젯을 클릭합니다. 이것은 앱 화면에서 위젯을 선택하고 위젯 트리를 해당 노드로 스크롤합니다. 위젯 모드 선택 버튼을 다시 토글하여 위젯 선택 모드를 종료합니다.

 

레이아웃 문제를 디버깅할 때 살펴봐야 할 핵심 필드는 size constraints필드입니다. 제약 조건은 트리 아래로 흐르고 크기는 다시 위로 흐릅니다.

 

Track widget creation

Flutter 관리자가 소스 코드에서 UI가 정의된 방식과 유사한 방식으로 위젯 트리를 표시할 수 있습니다. 

flutter run --track-widget-creation

 

--track-widget-creation 을 사용할 경우 다음과 같이 보기 편하게 구성됩니다.

 

--track-widget-creation 을 사용하지 않을 경우 위젯 트리의 깊이가 깊어져 보기 불편합니다.

 

 

 

3) Timeline view

기능

  • 프레임 렌더링 차트
  • 프레임 이벤트 차트
  • CPU 프로파일러

프레임 렌더링 차트

 

프레임 이벤트 차트

프레임 이벤트 차트는 단일 프레임에 대한 이벤트 추적을 보여줍니다. 최상위 이벤트는 그 아래에 이벤트를 생성하는 식입니다. UI 및 GPU 이벤트는 별도의 이벤트 흐름이지만 공통 타임라인을 공유합니다(프레임 차트 상단에 표시됨). 이 타임라인은 주어진 프레임에만 적용됩니다. 모든 프레임이 공유하는 시계는 반영하지 않습니다.

 

CPU profiler

프로파일러의 이 탭에는 선택한 프레임 이벤트(예: 다음 예의 레이아웃)에 대한 CPU 샘플이 표시됩니다. 이 차트는 맨 위의 스택 프레임이 아래의 스택 프레임을 호출하는 하향식 스택 추적으로 보아야 합니다. 각 스택 프레임의 너비는 CPU를 사용한 시간을 나타냅니다. CPU 시간을 많이 소비하는 스택 프레임은 가능한 성능 향상을 찾기에 좋은 위치일 수 있습니다.

 

call tree top-down
call tree view는 CPU profile에 대한 메서드 trace를 보여줍니다. 이 테이블은 profile을 top-down 으로 표현한 것입니다. 즉, callees 를 표시하도록 메서드를 확장할 수 있습니다 .

 


Total Time
메서드가 자체 코드와 호출 수신자에 대한 코드를 실행하는 데 소요한 시간입니다.


Self Time
메서드가 자체 코드만 실행하는 데 소요된 시간입니다.


Method
호출된 메서드의 이름입니다.


Source
메서드 호출 사이트의 파일 경로입니다.

 

call tree bottom-up

 

bottom up view는 CPU profile에 대한 메서드 추적을 보여주지만 이름에서 알 수 있듯이 profile의 bottom-up 표현입니다. 즉, 테이블의 각 최상위 메서드는 실제로 주어진 CPU 샘플에 대한 호출 스택의 마지막 메서드입니다(즉, 샘플의 리프 노드).

이 표에서 메서드를 확장하여 호출자 를 표시할 수 있습니다 .

 

Total Time

메서드가 자체 코드와 호출 수신자에 대한 코드를 실행하는 데 소요한 시간입니다.

 

Self Time

상향식 트리의 최상위 메서드(profile의 리프 스택 프레임)의 경우 메서드가 자체 코드만 실행하는 데 소요된 시간입니다. 하위 노드(CPU profile의 callees)의 경우 호출자가 호출할 때 호출 수신자의 자체 시간입니다. 다음 예에서 호출자의 자체 시간은 호출자 Element.updateSlotForChild.visit( )가 호출 [Stub] OneArgCheckInLineCache할 때 수신자의 자체 시간과 같습니다.

 

Method

호출된 메서드의 이름입니다.

 

Source

메서드 호출 사이트의 파일 경로입니다.

 

4) Memory view

클래스 생성자를 사용하여 생성된 할당된 Dart 개체(예: new MyClass()또는 사용 MyClass())는 이라고 하는 메모리 부분에 있습니다.

 

DevTools 메모리 창을 사용하면 특정 순간에 Isolate가 메모리를 어떻게 사용하는지 확인할 수 있습니다. (Isolate는 모든 Dart 코드가 실행되는 곳. 단일 스레드가 이벤트 루프를 실행하고 있다. ) 이 창은 Snapshot및 reset을 사용하여 accumulator count를 표시할 수 있습니다. 어플리케이션에서 memory leak이 의심되거나 memory allocation과 관련된 다른 버그가 있는 것으로 의심되는 경우 accumulator를 사용하여 메모리 할당 속도를 연구할 수 있습니다.

Memory profiling은 네 부분으로 구성되며 각 부분은 세분화됩니다.

  • Memory overview chart
  • Event timeline
  • Snapshot classes
  • Class instances

 

Memory anatomy

 

시계열 그래프를 사용하여 연속적인 시간 간격에서 Flutter 메모리의 상태를 시각화합니다. 차트의 각 데이터 포인트는 usage, capacity, external, garbage collection  resident set size와 같은 힙의 측정된 양(y축)과 timestamp(x축)에 해당합니다.

 

Legend

메모리와 관련하여 수집된 모든 측정값입니다. 범례 이름을 클릭하면 해당 데이터가 숨겨지거나 표시됩니다.

 

Range selector

수집된 모든 메모리 데이터(시계열). 선택기에서 가장 왼쪽 또는 첫 번째 시간/데이터(메모리 정보)는 어플리케이션이 시작된 때입니다. 가장 오른쪽 또는 마지막 시간/데이터는 어플리케이션이 중지될 때까지 수신(라이브)되는 연속 메모리 정보입니다.

 

Range selector view

이 시계열 범위(비회색 영역)에 대해 수집된 데이터의 상세 보기입니다.

 

X-axis timestamp

수집된 메모리 정보(capacity, used, external, RSS(resident set size), GC(garbage collection))의 시간.

 

Hover information of collected data

특정 시간(x축)에 상세하게 수집된 메모리 데이터.

 

Garbage collection occurred

Compaction of the heap 이 발생했습니다.

 

Event timeline

사용자 작업이 발생한 경우(예: 스냅샷 또는 재설정 버튼 클릭).

 

Snapshot

현재 활성 메모리 개체의 테이블을 표시합니다( 스냅샷 클래스 참조 ).

 

Reset accumulator

스냅샷 클래스 테이블의 누적기 열에서 값을 0으로 재설정합니다.

 

Filtering classes

보류 중

 

Snapshot classes

Snapshot 버튼(오른쪽 상단 영역)을 클릭하면 현재 메모리 개체의 테이블이 표시됩니다. 메모리 개체는 클래스 이름, 크기, 할당된 인스턴스 등으로 정렬할 수 있습니다.

 

Accumulator counts since reset

재설정 버튼(오른쪽 상단 영역)을 클릭하면 누적된 인스턴스 수가 재설정됩니다. 재설정 후 스냅샷을 클릭하면 마지막 재설정 이후 할당된 새 인스턴스 수가 표시됩니다. 이것은 메모리 누수를 찾는 데 유용합니다.

 

Class instances

스냅샷 클래스 테이블에서 클래스를 클릭하면 해당 클래스의 활성 인스턴스 수가 표시됩니다.

 

Inspecting contents of an instance

보류 중

 

Total active objects and classes in the heap

힙에 할당된 총 클래스 및 힙의 총 개체(인스턴스)입니다.

 

Memory overview chart

이 차트는 다양한 시점의 힙 상태를 시각화하는 데 도움이 되는 시계열 그래프입니다.

차트의 x축은 이벤트(시계열)의 타임라인입니다. 즉, 500ms마다 메모리의 폴링된 상태를 보여줍니다. 이것은 응용 프로그램이 실행될 때 메모리 상태에 대한 실시간 표시를 제공하는 데 도움이 됩니다. y축에 표시된 수량은 다음과 같습니다(위에서 아래로).

 

Capacity

힙의 현재 용량입니다.

 

GC

GC(가비지 컬렉션)가 발생했습니다. Dart가 가비지 수집을 수행하는 방법에 대한 자세한 내용은 Don't Fear Garbage Collector 를 참조하십시오 .

 

Used

힙의 개체(Dart 개체)입니다.

 

External

Dart 힙에 없지만 여전히 전체 메모리 사용 공간의 일부인 메모리입니다. external memory의 object는 native object입니다(예: 파일에서 읽은 메모리 또는 디코딩된 이미지). native object는 Dart embedder를 사용하여 native OS(Android, Linux, Windows, iOS 등)에서 Dart VM에 노출됩니다. embedder는 종료자가 있는 Dart 래퍼를 생성하여 Dart 코드가 이러한 기본 리소스와 통신할 수 있도록 합니다. Flutter에는 Android 및 iOS용 임베더가 있습니다. 

Dart on the Server 또는 Custom Flutter Engine Embedders를 참고

 

 

Event timeline

이 차트는 메모리 차트 타임라인과 관련된 DevTools 이벤트(예: snapshot 및 reset 버튼 클릭)를 표시합니다. 이벤트 타임라인의 마커 위로 마우스를 가져가면 이벤트가 발생한 시간이 표시됩니다. 이렇게 하면 타임라인(x축)에서 메모리 누수가 발생한 시기를 식별하는 데 도움이 됩니다.

 

snapshot 버튼을 클릭하면 모든 active class 및 해당 인스턴스와 관련된 힙의 현재 status가 표시됩니다. 이때 reset 버튼을 누르면 0으로 모든 클래스의 accumulator가 0으로 reset됩니다. reset은 파란색 수평 막대를 사용하여 이전 snapshot에 일시적으로 연결됩니다. reset 버튼을 다시 클릭하면 마지막 reset 이후 accumulator가 재설정되고 최근 reset이 이전 reset에 일시적으로 연결됩니다.

 

Snapshot classes

이 창에는 힙에 할당된 클래스, total instances, total bytes allocated 및 마지막 reset 이후 accumulator of allocations가 표시됩니다.

 

Size

힙의 현재 개체가 사용하는 총 메모리 양입니다.

 

Count

힙에 있는 현재 개체의 총 수입니다.

 

Accumulator

마지막 재설정 이후 힙에 있는 총 개체 수입니다.

 

Class

이 클래스에 할당된 개체의 집계입니다. 클래스 이름을 클릭하면 클래스 인스턴스 목록이 표시됩니다.

 

 

'개발 > Flutter' 카테고리의 다른 글

Flutter Firestore 구조  (1) 2021.12.31
Flutter + Firebase 채팅 기능 시연  (0) 2021.12.29
Flutter :: SharedPreference  (1) 2021.12.24
Flutter & Firebase :: QuerySnapshot vs DocumentSnapshot  (0) 2021.12.24
Flutter 2.8 발표  (0) 2021.12.23
728x90

 

Shared Preferences 사용법에 대해서 알아본다. Shared Preferences는 key-value 형태의 데이터를 디스크에 저장해서 사용하는 방법으로 기존의 안드로이드 앱 개발에서도 자주 사용되어 왔다. 로그인이 필요한 앱을 개발할 때 사용자의 ID와 패스워드 등을 기억하는 기능을 구현할 때 이용할 수 있다.

18 : getPref를 통해 key에 따른 value를 반환한다.

22 : key, value를 세팅한다.

 

728x90

 

QuerySnapshot

 

 

collection을 받아온다.

 

DocumentSnapshot

 

 

QuerySnapshot.docs를 사용하면 collection 안의 documents를 List<DocumentSnapshot>형태로 받아올 수 있다.

'개발 > Flutter' 카테고리의 다른 글

Flutter Devtools 개념 이해  (1) 2021.12.28
Flutter :: SharedPreference  (1) 2021.12.24
Flutter 2.8 발표  (0) 2021.12.23
Flutter에서 Null Safety를 사용하는 이유  (1) 2021.12.23
Flutter firebase 연동  (1) 2021.12.21
728x90

https://medium.com/flutter/announcing-flutter-2-8-31d2cb7e19f5

 

Announcing Flutter 2.8

A new release of Flutter: and a look back on a year of growth

medium.com

 

요약

며칠 전, 12월 9일에 Flutter 2.8 발표가 있었습니다.
저는 모바일 성능 향상과 WebView 3.0 업데이트 소식이 제일 반가웠는데요.
Flutter 2.8에서 크게 변화가 있었던 내용을 5가지로 정리해봤습니다.

1️⃣ 모바일 퍼포먼스
Flutter 2.8로 업그레이드하는 것만으로 앱이 더 빠르게 시작되고 더 적은 메모리를 사용한다고 합니다.

2️⃣ 로그인 위젯
Firebase를 사용하여 인증을 처리하는 로그인 위젯이 추가되었습니다. 이제 Google, Apple, Twitter 또는 Facebook을 인증을지원하는 복잡한 구현에 대해 전혀 걱정할 필요가 없습니다.

3️⃣ Flutter 모멘텀
현재 Play Store에 등록된 Flutter 앱은 375,000개 이상입니다. 지난 몇 달 동안 거의 2배 이상 급격하게 증가했습니다.

4️⃣ Flame
Flutter 기반으로 구축된 2D 게임 엔진 Flame 1.0이 릴리즈 되었습니다. Flame은 게임을 빠르게 구축하는 데 필요한 기능을 제공한다고 합니다.
https://flame-engine.org

5️⃣ Dart 2.15
Flutter 2.8에 Dart 2.15이 포함되었습니다. 덕분에 동시성(isolates)에 대한 주요 개선 사항을 포함하여 생성자 분리(constructor tear-off) 및 향상된 열거형(enum)와 같은 새로운 기능을 제공합니다.

 

 

728x90

 

Flutter 2.0 업데이트, null safety 적용

Flutter 2.0 업데이트 이후 dart 언어에 null safety가 적용되었다.

null으로 인한 런타임 에러를 방지하여 개발자의 생산성을 높일 수 있다.

유저가 실수로 체크하지 못한곳에서 발생하는 NullPointerException같은 에러를 조기에 방지하고
실제로 돌아가는 어셈블리상에서 null체크를 다시 안하기 때문에 성능적 이득도 있다.

 

자세히 말하면, null safety는 변수가 null을 허용하는지 구분하기 위한 개념인데 기본적으로 null을 허용하지 않는다.
dart 언어는 null safety를 지원하지 않았기 때문에
변수 값이 초기화 되지 않았거나 null 값을 전달하면 컴파일 에러는 발생하지 않고 런타임 과정에서 오류가 발생했다.
이러한 이유로 코드를 작성할 땐 오류가 발생할 것을 예상하지 못하기 때문에 코드를 수정하고 다시 빌드하기까지의 개발 시간이 추가된다.

 

이를 해결하기 위해 null safety를 적용하여
개발자가 런타임 과정이 아닌 코드를 편집하는 과정에서 IDE로 부터 빠른 피드백을 받도록 하였다.

 

 

첫 번째 그림처럼, 기존 null safety를 적용하기 전에는 null이 모든 유형의 하위유형으로 취급되었다.
이런 이유로 여러 자료형들이 null 값을 가질 수 있지만
여러 메서드가 정의되어 있는 다른 자료형과 달리 null는 메서드가 정의되지 않다.
따라서 상위 자료형에 null값이 대입되고 메서드를 적용하려 하면, null은 상위 자료형이 가진 메서드가 없기 때문에 런타임 에러가 발생하는 것이다.

두 번째 그림이 null safety가 적용된 Non-nullable 타입 구조이다.
Non-nullable에서는 null 자료형을 분리하여 다른 자료형들의 null을 허용하지 않는다.
하지만 자료형 뒤에 ?를 붙여 null 값을 허용하는 nullable 자료형을 제공하기 때문에 첫 번째 사진의 오류를 방지한다.

 

 

Null safety 적용

 

1) 버전 확인

Null safety는 Dart 2.12과 Flutter 2. 버전 부터 가능하다.

 dart --version

 

2) flutter를 stable 버전으로 업그레이드 한다.

flutter channel stable
flutter update

 

Null safety 문법

 

1) null 값을 사용하고 싶으면 자료형 뒤에 ?를 붙인다.

String? notAString = null; print(notAString?.length.isEven);

nullable : null값이 들어가는것을 허용 (ex: int?, String?, bool?)
non-nullable : null값이 들어가는것을 허용하지 않음 (ex: int, String, bool)

 

2) 

// 사용 불가
class ExampleData{ 
	String title; 
    Widget route; 
    
    ExampleData({this.title, this.route}); 
}

이 코드는 null safety가 적용된 이후 사용할 수 없습니다.

 

// 먼저 required를 사용하는 방법으로
class ExampleData {
  String title;
  Widget route;
  ExampleData({required this.title, required this.route});
  // @required가 아닌 required입니다
}

// Null값이 들어갈 수 있을 땐 ?를 붙여줍니다
class ExampleData {
  String? title;
  Widget? route;
  ExampleData({this.title, this.route});
}

// 초기값 부여
class ExampleData {
  String title;
  Widget route;
  ExampleData({this.title = '', this.route = const Spacer()});
}

대신 required를 사용하거나 null값이 들어갈 수 있다고 표시를 해주거나 default값을 줘야됩니다

기존에 사용하던 @required는 우리가 import하는 material.dart 혹은 coupertino.dart에 있었던 속성이고
앞으로 사용할 required는 언어 수준에서 제공하는 속성입니다

 

3) null이 들어가지 않는다고 확실히 말하고 싶다면 ! 키워드를 붙여준다

color? 형식의 BorderSide color에, null이 아닌 Color 자료형의 값을 넣어주니
Color? 자료형은 Color를 assign할 수 없다는 에러가 났다.
border 라이브러리 코드에서 color가 null이 아닐 때만 this.color를 내보내기 때문에 에러가 나는듯 하다.
null값이 들어가지 않다고 확실하게 말해주는 '!' 키워드를 붙여주니 해결되었다.

 

// null safety 해결 
BorderSide( width: 1, color: ColorPalette.primaryColor[400]!, );

 

// border 라이브러리 
BorderSide copyWith({Color? color, double? width, BorderStyle? style, }) { 
	assert(width == null || width >= 0.0); 
	return BorderSide( 
		color: color ?? this.color, 
		width: width ?? this.width, 
		style: style ?? this.style, ); 
}

 

4) 

// late속성 사용 
class _CounterState extends State<Counter> { 
	late int counter; 
    
	@override void initState() { 
		super.initState(); counter = 0; 
	} 
    
//null값 사용 
class _CounterState extends State<Counter> { 
	int? counter; 
        
	@override void initState() { 
		super.initState(); counter = 0; 
	}

이제부터는 late 속성을 사용하거나 null값이 들어갈 수 있다고 표시해줘야됩니다

 

 

'개발 > Flutter' 카테고리의 다른 글

Flutter & Firebase :: QuerySnapshot vs DocumentSnapshot  (0) 2021.12.24
Flutter 2.8 발표  (0) 2021.12.23
Flutter firebase 연동  (1) 2021.12.21
Flutter :: StreamBuilder vs FutureBuilder  (1) 2021.12.21
Flutter :: GetX 개념 이해  (0) 2021.12.21
728x90

fbtest0001

기본 crud 동작하는 버튼으로 컨트롤

 

create

// instance 가져오기
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
// 데이터 Create
firebaseFirestore
.collection('books')
.doc('flutter2')
.set({'page': 411, 'purchase?': false, 'title': '플러터 배우기'});

수행한 결과

 

read

// 데이터 Read
firebaseFirestore
.collection("books")
.doc("flutter2")
.get()
.then((DocumentSnapshot ds) {
  Map<String, dynamic> data = ds.data() as Map<String, dynamic>;
  title = data['title'];
  print(title);
}

 

update

// 데이터 Update
firebaseFirestore
.collection("books")
.doc("flutter2")
.update({"page": 543});

 

delete

// 데이터 delete
firebaseFirestore
.collection('books')
.doc('flutter2')
.update({'page': FieldValue.delete()});

 

728x90

 

streambuilder / futurebuilder는 firebase를 연동하며 자주 만날 수 있는 친구들이다.

firebase안에 data들이 있는데, 그것들이 변할 때, 정보를 계속적으로 읽는 역할을 한다.

 

Futurebuilder

한 번만 가져온다.

 

StreamBuilder

변화가 발생할 때마다 연속적으로 계속 가져온다

 

1) CollectionReference로 접근하여 특정 Document 하나의 data 읽기

Future<void> _getProduct() async {
      try {
        CollectionReference products =
            FirebaseFirestore.instance.collection('Products');

        await products
            .doc(arguments.docsName)
            .get()
            .then((DocumentSnapshot ds) {
          setState(() {
            _price = ds.data()['price'];
            _name = ds.data()['name'];
            _description = ds.data()['description'];
            _like = ds.data()['like'];
          });
        });
      } on firebase_storage.FirebaseException catch (e) {
        print(e);
      }
    }

 

2) DocumentReference로 접근하여 Data 읽기

Future<void> _getProduct() async {
      try {
        DocumentReference products = FirebaseFirestore.instance.collection("Products").doc(arguments.docsName);
        await products
            .get()
            .then((DocumentSnapshot ds) {
          setState(() {
            _price = ds.data()['price'];
            _name = ds.data()['name'];
            _description = ds.data()['description'];
            _like = ds.data()['like'];
          });
        });
      } on firebase_storage.FirebaseException catch (e) {
        print(e);
      }
    }

 

3) StreamBuilder사용하기

import 'package:Shrine/app.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:firebase_storage/firebase_storage.dart' as firebase_storage;

import 'home.dart';

class DetailPage extends StatefulWidget {
  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {

  @override
  Widget build(BuildContext context) {
    
    DetailArguments arguments = ModalRoute.of(context).settings.arguments;
    Record record = arguments.record;

    print("record Name:!! $record");

    Future<void> _deleteProduct() async {
      try {
        CollectionReference products =
            FirebaseFirestore.instance.collection('Products');
        products
            .doc(arguments.docsName)
            .delete()
            .then((value) => Navigator.pop(context));
      } on firebase_storage.FirebaseException catch (e) {
        ////e.code == 'canceled'
        print(e);
      }
    }

    Widget _buildContainer(
        BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
      return Column(
        children: <Widget>[
          SizedBox(height: 20),
          SizedBox(
            height: 230,
            width: double.infinity,
            //child: Image.network(imageURL),
            child: Image.network(snapshot.data.data()['imageURL']),
          ),
          Container(
              padding: EdgeInsets.all(50),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Row(
                    children: <Widget>[
                      Text(
                        "${snapshot.data.data()['name']}",
                        style: TextStyle(fontSize: 30),
                      ),
                      IconButton(
                        icon: Icon(Icons.thumb_up),
                        onPressed: () {
                          //TODO 좋아요 숫자.
                        },
                      ),
                      Text(
                        "${snapshot.data.data()['like'].toString()}"
                      ),
                    ],
                  ),
                  Text(
                    "\$ ${snapshot.data.data()['price'].toString()}",
                    style: TextStyle(fontSize: 20),
                  ),
                  Divider(
                    thickness: 3,
                  ),
                  Text(
                    record.description,
                  ),
                  SizedBox(height: 200),
                  Text(
                    "Creator: <${record.uid}>",
                  ),
                  Text(
                    "${snapshot.data.data()['creationTime'].toDate().toString()} Created",
                  ),
                  Text(
                    "${snapshot.data.data()['updateTime'].toDate().toString()} Modified",
                  ),
                ],
              )),
        ],
      );
    }

    return Scaffold(
        appBar: AppBar(
          title: Text("Detail"),
          centerTitle: true,
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.edit),
                onPressed: () {
                  } else {
                    print("It is not yours!!");
                  }
                }),
            IconButton(
                icon: Icon(Icons.delete),
                onPressed: () {
                    _deleteProduct();
                }),
          ],
        ),
        body: StreamBuilder<DocumentSnapshot>(
          stream: FirebaseFirestore.instance
              .collection("Products")
              .doc(arguments.docsName)
              .snapshots(),
          builder: (context, snapshot) {

            return _buildContainer(context, snapshot);
}

 

+ Recent posts