顯示具有 flutter 標籤的文章。 顯示所有文章
顯示具有 flutter 標籤的文章。 顯示所有文章

2023年3月22日 星期三

Flutter 開發筆記 - 使用 Admob - BannerAds 版型 @ Flutter 3.7.7, Android Studio 2022.1.1 Patch2, macOS 13.2.1




首先,透過 Android Studio 創建一個專案,預設跑 Build -> Flutter -> Build APK 可以正常跑東西:

flutter build apk

💪 Building with sound null safety 💪

Running Gradle task 'assembleRelease'...                           87.4s
✓  Built build/app/outputs/flutter-apk/app-release.apk (16.8MB).
Process finished with exit code 0

但是在 Project 視窗內,對 android 目錄 -> 右鍵 -> Flutter -> Open Android module in Android Studio 則是會看到:

Unsupported Java. 
Your build is currently configured to use Java 19.0.2 and Gradle 7.5.

Possible solution:
 - Open Gradle wrapper settings, change `distributionUrl` property to use compatible Gradle version and reload the project

解法就是 File -> Project structure -> Project

Android Gradle Plugin Version: 7.4.2
Gradle Version: 7.6.1

就可以很正常搞定了。

接著處理添加 Admob 的用法,參考 developers.google.com/admob/flutter/quick-start 流程:
  1. 先到原本的 flutter 專案上,選擇 terminal -> flutter pub add google_mobile_ads ,運行一陣子後,回去看 pubspec.yaml 可以看到添加了 google_mobile_ads: ^2.3.0 項目
  2. 對 flutter 專案的 Project 視窗內的 android 目錄 -> 右鍵 -> Flutter -> Open Android module in Android Studio ,進入開發單純的 Android 專案一樣,要進行 AndroidManifest.xml 設定,主要是添加 Admob 使用的資訊

<manifest>
    <application>
        <!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>
    <application>
<manifest>

接著用 Build -> Rebuild Project 會看到數個錯誤訊息:
  • Cannot fit requested classes in a single dex file (# methods: 68231 > 65536) 
  • Error: uses-sdk:minSdkVersion 16 cannot be smaller than version 19 declared in library [:google_mobile_ads]
解法就是在 build.gradle (Module: app) 更新:

defaultConfig {
    // ...

    minSdk 19
    multiDexEnabled true

    // ...
}

// ...

dependencies {
    // ...

    implementation "androidx.multidex:multidex:2.0.1"

    // ...
}

接著就是 Sync 以及 Build -> Rebuild Project ,更多資訊須參考 developer.android.com/studio/build/multidex?hl=zh-tw

3. 回到 flutter 專案上,編輯 lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

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

// ...

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  final BannerAd myBanner = BannerAd(
    // https://developers.google.com/admob/android/test-ads?hl=zh-cn#sample_ad_units
    // https://developers.google.com/admob/ios/test-ads?hl=zh-cn#demo_ad_units
    adUnitId: Platform.isAndroid ? 'ca-app-pub-3940256099942544/6300978111' : 'ca-app-pub-3940256099942544/2934735716' ,
    size: AdSize.banner,
    request: const AdRequest(),
    listener: BannerAdListener(
      // Called when an ad is successfully received.
      onAdLoaded: (Ad ad) => print('Ad loaded.'),
      // Called when an ad request failed.
      onAdFailedToLoad: (Ad ad, LoadAdError error) {
        // Dispose the ad here to free resources.
        ad.dispose();
        print('Ad failed to load: $error');
      },
      // Called when an ad opens an overlay that covers the screen.
      onAdOpened: (Ad ad) => print('Ad opened.'),
      // Called when an ad removes an overlay that covers the screen.
      onAdClosed: (Ad ad) => print('Ad closed.'),
      // Called when an impression occurs on the ad.
      onAdImpression: (Ad ad) => print('Ad impression.'),
    ),
  );

  @override
  void initState() {
    super.initState();
    myBanner.load();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(

        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              alignment: Alignment.center,
              width: myBanner.size.width.toDouble(),
              height: myBanner.size.height.toDouble(),
              child: AdWidget(ad: myBanner),
            ),
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

更多資訊: developers.google.com/admob/flutter/banner?hl=zh-cn (英文版顯示 404 NOT Found)

以上就順玩流程,但仍有很多細節未處理,像是廣告沒 load 到,或是 App 或 View/ViewController 切換時廣告資源釋放等等。

2023年3月15日 星期三

Flutter 開發筆記 - 撰寫平台相依性功能,以取得 Android Device 資料為例


研究了一下 Flutter 如何寫平台相依性的程式碼,看一眼也是常見的 Channel or Message 等溝通機制,也滿直觀的。以 Android 平台和 Kotlin 為例,先找到 MainActivity.kt ,接著,在 Code -> Override Methods 可以找到 configureFlutterEngine 可以添加

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
    }
}

接著就可以在透過 MethodChannel 建立綁定溝通管道:

import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val myCHANNEL = "samples.flutter.dev/helper"
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, myCHANNEL).setMethodCallHandler {
                call, result ->
            // This method is invoked on the main thread.
            // TODO

            if (call.method == "getBatteryLevel") {
                val batteryLevel = 0; // getBatteryLevel()
                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else if (call.method == "getDeviceInfo") {
                result.success(getDeviceInfo())
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getDeviceInfo(): HashMap<String, String> {
        val deviceInfo:HashMap<String, String> = HashMap<String, String>()
        deviceInfo.run {
            put("MODEL", Build.MODEL)
            put("MANUFACTURER", Build.MANUFACTURER)
        }
        return deviceInfo
    }
}

如此在 Flutter Dart 端,就可以呼叫:

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/helper');

  String _deviceInfo = "system info unknown";
  Future<void> _getDeviceInfo() async {
    Map<String, String> deviceInfo = {};
    try {
      final Map<Object?, Object?> result = await platform.invokeMethod('getDeviceInfo');
      deviceInfo.clear();
      result.forEach((key, value) {
        if (key.runtimeType == String && value.runtimeType == String) {
          deviceInfo[key.toString()] = value.toString();
        }
      });
    } on PlatformException catch (e) {
      deviceInfo.clear();
    } on Exception catch (e) {
      //print("Exception catch: $e ");
    }

    setState(() {
      _deviceInfo = json.encode(deviceInfo);
    });
  }
...

後續就只是完善細節,強烈建議直接觀看官方文件,有完整的細流程資訊和各平台的範例: docs.flutter.dev/development/platform-integration/platform-channels 

2021年2月3日 星期三

Flutter 開發文件之 iOS 與 Android 實作筆記 - 使用 Admob 以 Interstitial ads 為例


關於 flutter 使用 Admob 的架構,可參考網路上數篇文章:
此例筆記 iOS & Android 的設定方式,並且記錄碰到的錯誤訊息。

Android app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies (記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 google-services.json 檔案,擺在 flutter_app/android/app/ 中

3. 更新 flutter_app/android/build.gradle 的 buildscript -> dependencies 設定

//classpath 'com.android.tools.build:gradle:3.5.0'
// 為了修正錯誤訊息:error: unexpected element <queries> found in <manifest>.
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.google.gms:google-services:4.3.3'

4. 更新 flutter_app/android/gradle/wrapper/gradle-wrapper.properties 的 gradle 版本至 6.1.1

#為了修正錯誤訊息:Minimum supported Gradle version is 6.1.1. Current version is 5.6.2. If using the gradle wrapper
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

5. 增加 flutter_app/app/build.gradle 的 apply 項目

apply plugin: 'com.google.gms.google-services'

6. 更新 flutter_app/android/app/src/main/AndroidManifest.xml 添加 meta data (僅範例程式數值)

<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3940256099942544~3347511713"/>

7. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

iOS app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies(記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 GoogleService-Info.plist ,用 Xcode 打開專案,把 GoogleService-Info.plist 拖拉擺入跟 Info.plist 的同階目錄位置。

% open flutter_app/ios/Runner.xcworkspace
% ls flutter_app/ios/Runner/GoogleService-Info.plist

3. 更新 Info.plist 內容,添加資訊 GADApplicationIdentifier 等資訊(僅範例程式數值)

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>
<key>SKAdNetworkItems</key>
  <array>
    <dict>
      <key>SKAdNetworkIdentifier</key>
      <string>cstr6suwn9.skadnetwork</string>
    </dict>
  </array>

4. 更新 Podfile ,上方添加 platform :ios, '10.0'

為了修正錯誤訊息: 
Automatically assigning platform `iOS` with version `9.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile.

5. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

如此就收工啦!而 flutter_app/lib/main.dart 內容如下:

import 'package:flutter/material.dart';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_admob/firebase_admob.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  MobileAdTargetingInfo targetingInfo = MobileAdTargetingInfo(
    keywords: <String>['flutterio', 'beautiful apps'],
    contentUrl: 'https://flutter.io',
    childDirected: true,
    nonPersonalizedAds: true,
  );

  InterstitialAd _interstitialAd;

  InterstitialAd createInterstitialAd() {
    return InterstitialAd(
      adUnitId: InterstitialAd.testAdUnitId,
      targetingInfo: targetingInfo,
      listener: (MobileAdEvent event) {
        print("InterstitialAd event $event");
      },
    );
  }

  @override
  void initState() {
    super.initState();
    FirebaseAdMob.instance.initialize(appId: FirebaseAdMob.testAppId);
    //_interstitialAd = createInterstitialAd()..load();
  }

  @override
  void dispose() {
    _interstitialAd?.dispose();
    super.dispose();
  }

  void _incrementCounter() {
    _interstitialAd?.dispose();
    _interstitialAd = createInterstitialAd()..load()..show();
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

 

2021年1月22日 星期五

Flutter 開發文件之 iOS 與 Android 實作筆記 - 使用 Firebase 回報 App 使用人數


先說一下,有出類似文件教人怎樣使用 Firebase - Admob 部分,在此僅流水帳紀錄引用 firebase 的用法,體驗一下透過 Flutter 同時開發 ios 和 android app 時,並且使用額外函式庫的過程。

原理:

1. 使用 Android Studio + Flutter plugin 來建立 Flutter 專案
- 在此 package name 使用 org.changyy.study.flutter
- 自動產生 android Package Name = org.changyy.study.flutter_app
- 自動產生 iOS Bundle ID = org.changyy.study.flutterApp
- 假定專案位於 ~/AndroidStudioProjects/flutter_app

2. 建立 Firebase 專案
- 在此使用 org-changyy-study-flutter 作為 firebase project name

3. iOS app
- 使用 Xcode 置入 firebase 的設定檔案 GoogleService-Info.list
- 用指令打開專案 % open ~/AndroidStudioProjects/flutter_app/ios/Runner.xcworkspace/
- 使用 CocoaPods 添加 Firebase 函式庫
- 在 AppDelegate.swift 置入 Firebase 啟動程式碼

4. Android app
- 依照 Firebase 專案建置過程安置 google-service.json 檔案
- 在 <專案>/build.gradle 設置相依套件 com.google.gms:google-service:4.3.4
- 在 <專案>/app/build.gradle 添加 plugin 和引入 com.google.firebase:firebase-analytics

5. 回到 Flutter 專案,分別叫出 iOS simulator 跟 Android emulator 運行


若沒意外,就可以同時在這兩個模擬器上跑出來,並且在 Firebase 專案後臺,看到兩個使用者: