import 'dart:async';

import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart'; // Import the sensors package

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      debugShowCheckedModeBanner: false,
      home: SensorExample(),
    );
  }
}

class SensorExample extends StatefulWidget {
  @override
  _SensorExampleStats createState() => _SensorExampleStats();
}

class _SensorExampleStats extends State<SensorExample> {
  double _gyroX = 0.0;
  double _gyroY = 0.0;
  double _gyroZ = 0.0;

  double _accelX = 0.0;
  double _accelY = 0.0;
  double _accelZ = 0.0;

  double _barometerEvent = 0.0;

  double _magnX = 0.0;
  double _magnY = 0.0;
  double _magnZ = 0.0;
  @override
  void initState() {
    super.initState();

    barometerEventStream().listen((event) {
      setState(() {
        _barometerEvent = event.pressure;
      });
    });
    magnetometerEventStream().listen((event){
      setState(() {
        _magnX = event.x;
        _magnY = event.y;
        _magnZ = event.z;
      });
    });

    gyroscopeEventStream().listen((GyroscopeEvent event) {
      setState(() {
        _gyroX = event.x;
        _gyroY = event.y;
        _gyroZ = event.z;
      });
    });

    accelerometerEventStream().listen((event) {
      setState(() {
        _accelX = event.x;
        _accelY = event.y;
        _accelZ = event.z;
      });
    });
  }
  @override
  void dispose() {
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sensor Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Gyroscope Data:'),
            Text('gryoX: ${_gyroX.toStringAsFixed(2)}'),
            Text('gyroY: ${_gyroY.toStringAsFixed(2)}'),
            Text('gryoZ: ${_gyroZ.toStringAsFixed(2)}'),

            Text('Accelerometer Data:'),
            Text('accelX: ${_accelX.toStringAsFixed((2))}'),
            Text('accelY: ${_accelY.toStringAsFixed((2))}'),
            Text('accelZ: ${_accelZ.toStringAsFixed((2))}'),

            Text('Barometer Data:'),
            Text('baroPressure: ${_barometerEvent.toStringAsFixed(2)}'),

            Text('Magnetometer Data:'),
            Text('magnX: ${_magnX.toStringAsFixed(2)}'),
            Text('magnY: ${_magnY.toStringAsFixed(2)}'),
            Text('magnZ: ${_magnZ.toStringAsFixed(2)}'),
          ],
        ),
      ),
    );
  }
}

flutter/dart에서 사용가능한 패키지 중 하나인 sensors_plus에 있는 가속도계, 기압계, 자이로스코프, 자력계를 화면에 표시하는 방법

class AppBannerAdManager {
  BannerAd? _bannerAd;
  bool _isAdLoaded = false;

  VoidCallback? onAdLoadedCallback;

  AppBannerAdManager({this.onAdLoadedCallback});

  void loadBannerAd() {
    _bannerAd = BannerAd(
      adUnitId: 'ca-app-pub-3940256099942544/9214589741', // 실제 광고 단위 ID로 변경
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener(
        onAdLoaded: (Ad ad) {
          _isAdLoaded = true;
          print('배너 광고 로드 성공');
          // 광고 로드 시 상태 업데이트
          // 이때 setState를 호출하려면 해당 클래스가 StatefulWidget이어야 합니다.
          // 하지만 AppBannerAdManager는 그렇지 않으므로, 콜백으로 상태 업데이트를 전달해야 합니다.
          onAdLoadedCallback?.call();
        },

        onAdFailedToLoad: (Ad ad, LoadAdError error) {
          ad.dispose();
          print('배너 광고 로드 실패: $error');
        },
      ),
    );

    _bannerAd!.load();
  }

광고 로드 및 리소스 해제 기능을 제공하는 class

BannerAd? _BannerAd: google_mobile_ads 패키지에 있는 객체. ?는 null일 수 있음을 의미한다.

VoidCallback? onAdLoadedCallback: 광고가 로드되었을 때 호출될 콜백 함수.

AppBannerAdManager({this.onAdLoadedCallback}): 생성자. onAdLoadedCallback을 배개변수로 받음.

void loadBannerAd(): 배너 광고를 초기화하고 로드하는 역할

_bannerAd = BannerAd(...) : 새로운 BannerAd 인스턴스 생성

listenr: BannerAdListener: 광고 로드 상태를 모니터링하는 리스너

onAdLoaded: 광고가 정상적으로 로드됐을 경우. onAdLoadedCallback을 호출. ApBannerAdManager는 StatefulWidget이 아니므로 setState가 아니라 callback을 통해 상태 업데이트를 전달한다.

onAdFailedToLoad: 로드 실패했을 때, dispose로 리소스를 해제한다.

_bannerAd!.load(): 광고를 실제로 로드하는 method. 여기서 로드에 성공하면 listener 처리가 시작된다.

double? get bannerHeight => _isAdLoaded?_bannerAd?.size.height.toDouble() : 0: 광고가 로드되면 광고 높이 확인

Widget getBannerWidget(): 배너 광고가 로드된 경우 광고를 표시하는 위젯을 반환. 로드 실패 시, 빈 위젯 반환

Container: 광고를 감싸는 컨테이너 위젯

child: AdWidget(ad: _bannerAd!): 광고 객체를 전달받아서 표시. Container안에 bannerAd를 child로 두고 표시.

SizedBox.shrink(): 빈 공간을 차지하지 않는 위젯

 

class AppOpenAdManager {
  AppOpenAd? _appOpenAd;
  bool _isShowingAd = false;

  void loadAd() {
    AppOpenAd.load(
      adUnitId: 'ca-app-pub-3940256099942544/3419835294', // 실제 광고 단위 ID로 변경하세요
      request: const AdRequest(),
      adLoadCallback: AppOpenAdLoadCallback(
        onAdLoaded: (ad) {
          _appOpenAd = ad;
          debugPrint('앱 오프닝 광고 로드 성공');
          showAdIfAvailable();
        },
        onAdFailedToLoad: (error) {
          debugPrint('앱 오프닝 광고 로드 실패: $error');
        },
      ),
      //orientation: Orientation.portrait,
    );
  }

  void showAdIfAvailable() {
    if (_isShowingAd || _appOpenAd == null) {
      debugPrint('광고를 표시할 수 없습니다.');
      return;
    }

    _appOpenAd!.fullScreenContentCallback = FullScreenContentCallback(
      onAdShowedFullScreenContent: (ad) {
        _isShowingAd = true;
        debugPrint('앱 오프닝 광고가 표시되었습니다.');
      },
      onAdDismissedFullScreenContent: (ad) {
        _isShowingAd = false;
        _appOpenAd = null;
        debugPrint('앱 오프닝 광고가 닫혔습니다.');
        loadAd(); // 광고를 다시 로드
      },
      onAdFailedToShowFullScreenContent: (ad, error) {
        _isShowingAd = false;
        _appOpenAd = null;
        debugPrint('앱 오프닝 광고 표시 실패: $error');
        loadAd(); // 광고를 다시 로드
      },
    );

    _appOpenAd!.show();
  }
}

AppOpenAd: google_mobile_ads 패키지에 있는 객체

ad 객체에 대한 설명은 -5 문서 참고.

void showAdIfAvaliable: 광고가 로드되었고 광고가 표시되어있지 않은 경우 광고를 화면에 표시

_appOpenAd!.fullScreenContentCallback = FullScreenContentCallback(...)

FullScreenContentCallback : 광고 전체 화면 콘텐츠 상태 변화를 처리하는 콜백 클래스

onAdShowedFullScreenContent: 광고가 전체 화면으로 표시되었을 때 호출됨

onAdDismissedFullScreenContent : 광고가 닫혔을 때 호출됨

onAdFailedToShowFullScreenContent: 광고를 표시하는데 실패했을 때 호출됨

 

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  final AppOpenAdManager _appOpenAdManager = AppOpenAdManager();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _appOpenAdManager.loadAd();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _appOpenAdManager.showAdIfAvailable(); // 앱이 재개될 때 광고를 표시
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Application Title',
      theme: ThemeData(
        primaryColor: Colors.indigo,
        colorScheme: ColorScheme.fromSwatch(
          primarySwatch: Colors.indigo,
          accentColor: Colors.amber,
        ),
        textTheme: const TextTheme(
          bodyLarge: TextStyle(fontSize: 16.0, color: Colors.black87),
          bodyMedium: TextStyle(fontSize: 14.0, color: Colors.black54),
          titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold, color: Colors.indigo),
        ),
        buttonTheme: ButtonThemeData(
          buttonColor: Colors.amber,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(20.0),
          ),
        ),
      ),
      home: const LinkListPage(),
    );
  }
}

_MyAppState는 State 클래스를 상속받아서 MyApp 위젯의 상태를 관리하는 클래스이다. 

with WidgetsBindingObserver: WidgetsBindingObserver 믹스인을 사용하여 앱의 라이플사이클을 관찰할 수 있다고 한다. 뭔말인지 모르겠다. _MyAppState에서 또 호출하는 위젯들의 상태를 관찰하고 setState를 할 수 있다는 의미인건지 확인이 필요하다.

AppOpenAdManager는 오픈형 앱 광고 인스턴스 생성하는 줄이므로 이번 글에선 생략

void Initiate(): 상태가 초기화될때 호출되는 method.

super.initiate(): 부모(State)의 initiate를 호출하여 기본 초기화 작업 수행

WidgetsBinding.instance.addObserver(this): _MyAppState 클래스를 옵저버로 등록하여 앱의 라이프사이클을 관찰할 수 있게 한다.

_appOpenAdManager.loadAd() 도 광고 관련이므로 이번 글에선 생략

dispose() : 상태가 제거될 때 호출되는 method. 상태가 제거된다는게 어떤 의미인지 확인이 필요하다. _MyAppState는 MyApp 바로 다음의 클래스니까 앱이 꺼지는 상태를 말하는 것일까?

WidgetsBinding.instance.removeObserver(this): 옵저버 목록에서 _MyAppState를 제거. 더이상 앱상태를 관찰하지 못함.

super.dispose(): State의 dispose 호출,

didChangeAppLifecycleState(): 앱의 라이프사이클 상태가 변경될 때 호출되는 method.

AppLifecycleState.resumed: 앱이 백그라운드에서 포그라운드로 전환될 때의 상태라고 한다. flutter 앱 라이프사이클은 별도로 볼 필요가 있다.

_appOpenAdManager.showAdIfAvailable(): 생략

Widget build() : 실제 UI를 구성하는 method. 각 프레임(화면)마다 호출이 되어 위젯 트리(오브젝트를 트리 형태로 구성)를 구성한다.

return MaterialApp: MaterialApp 생성자를 호출하여 새로운 MaterialApp 위젯을 생성.

title은 실제 화면에서는 안보이므로 무시해도 된다. 어디서 보이는지 모르겠다. 창 제목이라는데 일단 안보인다.

theme: ThemeData: 앱의 전반적인 테마를 설정한다.

primaryColor: 앱의 기본 색상

colorScheme: 앱의 색상 체계 fromSwatch는 스와치를 기반으로한 생상 체계를 생성하는 팩토리 생성자. 스와치가 뭔지는 별도로 찾아봐도 될 듯 하다.

textTheme: 텍스트 스타일 정의

buttonTheme: 버튼 기본 스타일 정의. 내부 디자인 설명은 굳이..알아야 할까 어차피 할때마다 다를텐데

home: construction LinkListPage(): 앱의 기본 홈 화면을 설정한다. LinkListPage는 사용자가 처음 보게되는 화면이다.

LinkLIstPage를 블로그에 올려도 될런지 모르겠다.

 

 

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:html/parser.dart' show parse;
import 'package:webview_flutter/webview_flutter.dart';
import 'package:printing/printing.dart';
import 'package:http/http.dart' as http;
import 'package:google_mobile_ads/google_mobile_ads.dart';

이번 프로젝트에서 사용된 package들.

dart:convert: JSON 인코딩/디코딩 및 Base64 인코딩을 위해 사용. 사용한 부분이 있는지 기억이 잘안난다. 아마 파싱할 때 같이 쓰인 것 같다. 나머지는 뭐... 이름 그대로다.

google_mobile_ads 는 애드몹을 위해서 사용하는 패키지.

http는 http 요청을 보내는 용도, printing은 html내용을 pdf로 변환하기 위한 용도. 

webview_flutter는 웹페이지를 앱 내에서 표시하는 패키지. html/parser는 파싱용. 많이 썼다.

void main(){
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();
  runApp(const MyApp());
}

WidgetFlutterBinding.ensureInitialized(): flutter 엔진 초기화.앱을 실행하기 전에 필요한 바인딩을 설정한다. WidgetsFlutterBinding의 인스턴스가 초기화되어있는지 확인한다. 초기화되지 않았다면 새로운 인스턴스를 생성하고 호출한다.

flutter/widgets.dart 또는 flutter/material.dart에 있다고 하다는데 cupertino에서도 쓰는걸 보면 그냥 처음에 이거 없으면 안되는 것 같다. 이유불문 있어야 한다고 생각하자. void Main()이 있어야 하는 이유와 다를 바가 없는 듯.

MobileAds.instance.initialize(): 설명 많이 할 거 없고 구글 애드몹 SDK 초기화 및 인스턴스 초기화.

runApp(const MyApp());

주어진 위젯(MyApp())을 루트 위젯으로 설정하고, 화면에 렌더링한다.  const가 굳이 붙어야 하냐 하면 잘모르겠긴 하다. 불변 객체로 생성하기 위함이라는데 음... 조금 더 확인이 필요해보인다.

어쨌건 이를 통해서 void main()에는 처음 인스턴스 초기화 및 초기 위젯 설정 외에 다른 역할을 하지 않는다는 것을 알 수 있다.

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

MyApp을 위젯이라 하는 이유는 StatefulWidget을 상속받기 때문이다. StatefulWidget은 상태를 가질 수 있는 위젯이다. 상태가 변경될 때마다 위젯을 다시 빌드할 수 있어서 동적 UI를 구현하는데 적합하다고 한다. 대부분의 앱들은 사실상 StatefulWidget으로 만들어지지 않을까 싶다. 그럼 StatelessWidget의 장점이 무엇일지 찾아보는 것도 좋아보인다.

const MyApp({super.key}) : MyApp 클래스 생성자. const인 이유는 불변 객체로 생성하여 성능 최적화를 도모하기 위함이라고 한다.

super.key: Flutter에서 위젯을 식별하고 효율적으로 업데이트할 수 있도록 돕는 Key 객체를 설정한다고 한다. 뭔 말인지 잘 모르겠다. 흠..

createState(): _MyAppState 클래스의 새 인스턴스를 생성하여 반환한다. _MyAppState는 MyApp의 상태를 관리하며, 실제로 UI를 빌드하고 상호작용을 처리한다. 

_MyAppState는 State<MyApp>을 상속받아, MyApp 위젯의 상태를 정의하고 관리한다. _MyAppState 내에서 setState()가 발생할 때 위젯을 다시 그린다고 생각하면 되는 것 같다. 그래서 값을 변경하는 경우가 있을 때 보통 setState 안에 담아서 처리한다.

 

애드몹 앱ID를 info.plist에 등록했는지 확인해봅시다. 앱id는 안드로이드, 애플 별도로 받아야 하기 때문에 볃도로 앱을 하나 더 등록하고 받은 앱id를 사용해서 작성해야 합니다. 

 

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>

해당 항목이 있는지 확인 꼭 해보도록 한다. 내 3일이 날아갔었다..

'flutter' 카테고리의 다른 글

flutter 앱 개발 소스 분석-3  (0) 2024.12.12
flutter 앱 개발 소스 분석-2  (0) 2024.12.11
flutter 앱 개발 소스 분석-1  (1) 2024.12.11
flutter build appbundle 오류  (0) 2024.11.26
flutter 1일차  (0) 2024.11.24

9: only buildscript {}, pluginManagement {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed

처음에 flutter 프로젝트를 생성하면 plugin이 {}에 묶여있다.

 

plugins {
    id "com.android.application"
    id "kotlin-android"
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
    id "dev.flutter.flutter-gradle-plugin"
}

 

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

근데 요렇게 하고 key.properties를 가져오는 코드를 넣고 build appbundle을 하면 오류가 발생한다.

해결법은 plugins {} 안의 내용물을 다 바깥으로 뺀 후에 최상단에 놓으면 된다는거다.

apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "dev.flutter.flutter-gradle-plugin"

왜인지 누가 좀 알려줘,,,,

String? name;

factory

현재 이해 안가는 두가지.

글 간격이 왜이렇게 넓은지 한 번 확인해봐야겠다.

+ Recent posts