😎

Flutterでネイティブ広告(ネイティブアドバンス)を使ってみる(Android)

2024/04/25に公開

はじめに

自分はグリッドビューを使っていてその中で広告を表示させようとしていました。今までバナー広告を使っていたのですが、グリッドビューだと一つのグリッドが縦長になってしまうからかうまく広告が表示されませんでした。そこでいろいろ調べてみて、カスタマイズ性が高い?ネイティブ広告ならなんとかできるのかなと思い実装してみました。

元々NativeAdの方にはnativeTemplateStyleというものがあり、これを使うのでもいいとは思うのですが、どうせならカスタマイズする方法を知りたいということで調べました。

自分はネイティブ(KotlinとかSwift)に触ったことがないのとAndroid StudioでなくVSCodeで開発しているからか、思ったより面倒だったので備忘録としてまとめておきます。

あまりわかっていない部分があるので参考程度にしてください。
間違っているところだったり改善点だったりがあり、かつ暇であれば指摘をお願いします。

以下では、すでにGoogle Admobの方でネイティブアドバンス広告のユニットIDは取得済みとします。

環境

Flutter 3.19.5
google_mobile_ads: ^5.0.0

公式など参考

Codelabs
https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter#7
ここが一番詳しいと思いますがAndroid Studioを使わないのでディレクトリがよくわからなかったです。

https://itnext.io/flutter-native-ads-92d802fbd927
これはCodelabsのまとめ的なものでディレクトリがわかります。

https://developers.google.com/admob/flutter/native/platforms?hl=ja#android
ここも公式だとは思いますが、ちょいちょい違うのでよくわかりませんでした。settings.gradleとか。

基本的に上二つを参考にして、layoutを決めるxmlファイルは頑張って決める感じです。ChatGPTとかに聞く感じで。

ディレクトリ

layoutを決めるxmlファイルについて

MainActivityとFactoryを入れるディレクトリ(今回見せるのはLightFactoryとMainActivityのみ)

公式では、
list_tile_native_ad.xml、ListTileNativeAdFactory.kt、MainActivity.kt、となっているファイルを自分好みに編集して、Flutterの方で使います。

自分の場合、前者二つを、grid_native_ad_light.xml、GridNativeAdLightFactory.ktという名前にして編集しました。

ネイティブでの編集

grid_native_ad_light.xml

grid_native_ad_light.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.gms.ads.nativead.NativeAdView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"  
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/iv_grid_native_ad_light_icon"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:scaleType="fitCenter"
            tools:background="#EDEDED"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHeight_percent="0.6"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <TextView
            android:id="@+id/tv_grid_native_ad_light_attribution_small"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#F19938"
            android:text="Ad"
            android:textColor="#FFFFFF"
            android:padding="2dp"
            android:textSize="14sp"
            android:layout_marginTop="2dp"
            app:layout_constraintTop_toBottomOf="@id/iv_grid_native_ad_light_icon"
             />

        <TextView
            android:id="@+id/tv_grid_native_ad_light_attribution_large"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#F19938"
            android:gravity="center"
            android:text="Ad"
            android:textColor="#FFFFFF"
            android:visibility="invisible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="8dp"
            app:layout_constraintTop_toBottomOf="@id/tv_grid_native_ad_light_attribution_small"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

            <TextView
                android:id="@+id/tv_grid_native_ad_light_headline"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:lines="1"
                android:textColor="#007BFF"
                android:textSize="16sp"
                tools:text="Headline" />

            <TextView
                android:id="@+id/tv_grid_native_ad_light_body"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:lines="1"
                android:textColor="#828282"
                android:textSize="14sp"
                tools:text="body" />

        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.gms.ads.nativead.NativeAdView>

正直なところレイアウトはよくわかっていないのですが上記のように自分はしています。
ImageViewで画像を上の方に表示し親要素、ここではグリッドの高さの60%を占めるようにしています。
また広告であることを示すための「Ad」はImageViewのIdを取ってその左下に置くようにしています。
後はその下に説明文を載せている感じです

ここが一番面倒でした。
以下のGridNativeAdLightFactory.ktで参照します。

GridNativeAdLightFactory.kt

GridNativeAdLightFactory.kt
package com.example.hogehoge
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.google.android.gms.ads.nativead.NativeAd
import com.google.android.gms.ads.nativead.NativeAdView
import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin

class GridNativeAdLightFactory(val context: Context) : GoogleMobileAdsPlugin.NativeAdFactory {

    override fun createNativeAd(
            nativeAd: NativeAd,
            customOptions: MutableMap<String, Any>?
    ): NativeAdView {
       // val nativeAdView = LayoutInflater.from(context)
        //        .inflate(R.layout.grid_native_ad_light, null) as NativeAdView

        val nativeAdView = LayoutInflater.from(context)
                .inflate(R.layout.grid_native_ad_light, null, false) as NativeAdView


        with(nativeAdView) {
            val attributionViewSmall =
                    findViewById<TextView>(R.id.tv_grid_native_ad_light_attribution_small)
            val attributionViewLarge =
                    findViewById<TextView>(R.id.tv_grid_native_ad_light_attribution_large)

            val iconView = findViewById<ImageView>(R.id.iv_grid_native_ad_light_icon)
            val icon = nativeAd.icon
            if (icon != null) {
                attributionViewSmall.visibility = View.VISIBLE
                attributionViewLarge.visibility = View.INVISIBLE
                iconView.setImageDrawable(icon.drawable)
            } else {
                attributionViewSmall.visibility = View.INVISIBLE
                attributionViewLarge.visibility = View.VISIBLE
            }
            this.iconView = iconView

            val headlineView = findViewById<TextView>(R.id.tv_grid_native_ad_light_headline)
            headlineView.text = nativeAd.headline
            this.headlineView = headlineView

            val bodyView = findViewById<TextView>(R.id.tv_grid_native_ad_light_body)
            with(bodyView) {
                text = nativeAd.body
                visibility = if (nativeAd.body.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
            }
            this.bodyView = bodyView

            setNativeAd(nativeAd)
        }

        return nativeAdView
    }
}




ここでgrid_native_ad_light.xmlファイルを参照しています。

MainActivity.kt

MainActivity.kt
package com.example.hogehoge

import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

元々上記であったファイルを

MainActivity.kt

package com.example.hogehoge

import android.content.Context
import android.view.LayoutInflater
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin

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

        val gridNativeLightFactory = GridNativeAdLightFactory(context)
        GoogleMobileAdsPlugin.registerNativeAdFactory(flutterEngine, "gridLight", gridNativeLightFactory)

    }

    override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
        super.cleanUpFlutterEngine(flutterEngine)
        
        // Factoryを登録解除
        GoogleMobileAdsPlugin.unregisterNativeAdFactory(flutterEngine, "gridLight")

    }
}



のように変更しました。ここで先ほど作成したGridNativeAdLightFactory.ktを使っているんですね。

Flutter側での処理

grid_native_ads.dart
import 'package:your_project/env/env.dart';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class GridNativeAdWidget extends StatefulWidget {
  final double height;
  final double width;

  const GridNativeAdWidget({
    super.key,
    required this.height,
    required this.width,
  });

  
  _GridNativeAdWidgetState createState() => _GridNativeAdWidgetState();
}

class _GridNativeAdWidgetState extends State<GridNativeAdWidget> {
  NativeAd? _nativeAd;
  bool _isAdLoaded = false;

  void _loadNativeAd() {
    _nativeAd = NativeAd(
      adUnitId: Env.n1,  // 広告ユニットID、自分はenviedを使っている

      //factoryIdを指定しない場合にはnativeTemplateStyleを使う。
      factoryId: 'gridLight',  // MainActivity.ktで指定したfactoryId

      //アスペクト比
      nativeAdOptions: NativeAdOptions(
        mediaAspectRatio: MediaAspectRatio.any,
      ),

      listener: NativeAdListener(
        onAdLoaded: (_) {
          setState(() {
            _isAdLoaded = true;
          });
        },
        onAdFailedToLoad: (ad, error) {
          ad.dispose();
        },
      ),
      request: const AdRequest(),
      
    );

    _nativeAd!.load();
  }

  
  void initState() {
    super.initState();
    _loadNativeAd();
  }

  
  void dispose() {
    _nativeAd?.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: _isAdLoaded ? AdWidget(ad: _nativeAd!) : Container(),
    );
  }
}

重要な部分を抜き出します。

void _loadNativeAd() {
    _nativeAd = NativeAd(
      adUnitId: Env.n1,  // 広告ユニットID、自分はenviedを使っている

      //factoryIdを指定しない場合にはnativeTemplateStyleを使う。
      factoryId: 'gridLight',  // MainActivity.ktで指定したfactoryId

      //アスペクト比
      nativeAdOptions: NativeAdOptions(
        mediaAspectRatio: MediaAspectRatio.any,
      ),

      listener: NativeAdListener(
        onAdLoaded: (_) {
          setState(() {
            _isAdLoaded = true;
          });
        },
        onAdFailedToLoad: (ad, error) {
          ad.dispose();
        },
      ),
      request: const AdRequest(),
      
    );

    _nativeAd!.load();
  }

ここでfactoryIdを先ほどMainActivity.ktで書いた、gridLightに設定することで、自分の好きなlayoutを呼び出すことができます。

factoryIdを使わないのであればnativeTemplateStyleを使うこととなります。
どちらか一方は必ず非nullでないといけません。

実際にグリッドで表示すると次のようになりました。

グリッド1

グリッド2

グリッド3

あとがき

テンプレートを使えばある程度はいいのでしょうが、カスタマイズするにはどうすればいいのかを調べてみると結構面倒でした。とくにxmlファイルでレイアウトをどう制御するのかがあまりわからずに苦戦しました。

ネイティブについてはまだあまりよくわかっていないのでもう少し勉強したいです。

VSCodeの拡張機能とかでAndroid Studioのビューワーみたいなのってないんですかね?以下のサイトみたいな感じで
https://qiita.com/dosukoi_android/items/d72c2f6d668ef04206ac

勉強はこれとかよさそう
https://qiita.com/tktktks10/items/62d85dabac4bdb8c1f94#垂直方向に好きな位置

Discussion