しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

FlutterでHello World

Flutterを使ってiPhone/Androidアプリを試してみました。

SDKのインストール

まずは下サイトを参考にSDKをダウンロードします。
ダウンロードしたら適当な場所に配置してパスを通します。

docs.flutter.dev

パスを通したら下コマンドで問題ないかチェックします。

flutter doctor

今回は下のような結果になりました。
2つエラーが出ていたので書かれている指示に従って対応します。

対応したら下のように全て緑になりました。

Flutterアプリ作成

アプリの作成は下コマンドです。

flutter create my_app

コマンドを実行すると下のような構成でプロジェクトが作られます。

iPhoneシミュレータで動かしたいので下コマンドでシミュレータを起動します。

open -a Simulator

プロジェクトのフォルダに移動して下コマンドを打つことでflutterの実行ができます。

flutter run

これで下のようにシミュレータ上でアプリが起動しました。

次はAndroidでのアプリ立ち上げも検証します。
まずはAndroid Studioを開いてエミュレータを実行します。

その状態でflutter runコマンドを打つと下のようにデバイス選択画面になるのでAndroidエミュレータを選びます。

少し待つと下のようにアプリが起動しました。

Flutterアプリの編集

次はアプリの編集も少しやってみたいと思います。
今回はAndroid Studioを使って編集します。
Android Studio以外にもVSCodeなどいくつか対応しているIDEがあるようです。

まずはAndroid Studioの最初の画面のPluginsの画面に移動します。

下の方にFlutterのプラグインがあるのでインストールして再起動します。

次はAndroid Studioで先程作ったFlutterプロジェクトを開きます。
開くと下のようなプロジェクト構成になっているのが分かります。

Android Studio画面の右上からシミュレータなどを選んで実行する事が可能です。

最後にこのアプリのナビゲーションバーのタイトルを変更してみます。
main.dartを開きMyAppクラスのMyHomePageを作っている箇所のタイトルを変更します。

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'MyApp'),
    );
  }
}

変更すると下のようにタイトルが更新されました。

iOS16のロック画面のウィジェットを試してみる

iOS16でロック画面にウィジェットが出せるようになったので試してみました。

まずは通常のアプリ同様にプロジェクトを作成します。

次はプロジェクト設定のTARGETS下部にあるプラスボタンからターゲットを追加します。

追加するのはWidget Extensionです。

名前を付けて作成します。

途中でSchemeを追加するか聞かれるので追加します。
追加する事で下のようにウィジェットを選んで立ち上げできるようになります。

実行すると下のようにホーム画面にウィジェットが追加される事が分かります。

次はロック画面にもウィジェットを追加していきます。

サポートするためにWidgetConfigurationにsupportedFamiliesを追加する必要があります。

@main
struct MyAppWidget: Widget {
    let kind: String = "MyAppWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            MyAppWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.accessoryRectangular])
    }
}

iOS15以下はaccessoryRectangularが使えないので下のように分岐を入れる必要があります。

@main
struct MyAppWidget: Widget {
    let kind: String = "MyAppWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            MyAppWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies(supportedFamilies())
    }
    
    func supportedFamilies() -> [WidgetFamily] {
        if #available(iOSApplicationExtension 16.0, *) {
            return [.accessoryRectangular]
        } else {
            return [.systemSmall]
        }
    }
}

iOSでサポートされている形式は下の3つです。

accessoryInline accessoryCircular accessoryRectangular
日付の横(時刻の右上に表示されるウィジェット) 時刻の下に表示される、円形 時刻の下に表示される、四角で円形2個分のサイズ

ウィジェットの実装は下の通りです。
実装はホーム画面のウィジェット同様です。

struct MyAppWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text("ウィジェットテスト").frame(maxWidth: .infinity, maxHeight: .infinity).background(.black)
    }
}

widgetFamilyによって表示を変える場合は下のようにします。

struct MyAppWidgetEntryView : View {
    @Environment(\.widgetFamily) var family
    var entry: Provider.Entry

    var body: some View {
        switch family {
        case .accessoryInline: Text("accessoryInline").frame(maxWidth: .infinity, maxHeight: .infinity).background(.black)
        case .accessoryCircular: Text("accessoryCircular").frame(maxWidth: .infinity, maxHeight: .infinity).background(.black)
        case .accessoryRectangular: Text("accessoryRectangular").frame(maxWidth: .infinity, maxHeight: .infinity).background(.black)
        default: Text("")
        }
    }
}

AndroidのソースコードをMacにダウンロード

Android開発時にソースコードを見たい時があったので見方を調べてみました。
Android Studioから見る事もできるのですが、こちらだと関連メソッド見るのが少し手間だったりしたのでソースコードのダウンロードする方法を調べました。

ソースコードのDLはrepoというツールを使います。
repoはGitを補完するツールで複数のGitリポジトリの管理などができるようです。
MacでのインストールはBrewを使います。

brew install repo

次はリポジトリの初期化を行います。
ソースコマンドをダウンロードしたいフォルダで下コマンドを実行する事で初期化します。

repo init -u https://android.googlesource.com/platform/manifest

初期化が完了すると下のように.repoフォルダが作られます。

最後にソースコードのダウンロードします。
ダウンロードのコマンドは下のとおりです。

repo sync

ダウンロードは今回は2時間ほどかかりました。
ソースコードの容量は143GBです。
2011年の記事では9GBほどだったと書かれているのでかなりプロジェクトが大きくなったようです。

また、ソースコードは下のサイトで見る事も可能です。
使い勝手がIDEに近くて使いやすいので基本的にはこちらを見るほうが良さそうです。

https://cs.android.com/?hl=ja

参考URL

ソースコード検索  |  Android オープンソース プロジェクト  |  Android Open Source Project

Androidのソースコードを取得する方法

Swift5.5のasync/awaitを試してみる

Swift5.5で登場したasync/awaitを試してみました。
async/awaitは非同期処理を簡単に書けるような機能で通信処理などの実装を書きやすくなります。

今までは通信などの非同期処理を実装する時、下のように完了後のメソッド(completion)を使って実装する事が主流でした。

func asyncMethod(completion: () -> ()) {
    // 通信などの非同期処理
    completion()
}

asyncMethod(completion: {
    // 完了後の処理
})

async/awaitを使って書き直すと下のようになります。
完了後の処理がcompletionではなくasyncMethodのすぐ下に書けるようになりました。

func asyncMethod() async {
    // 通信などの非同期処理
}

Task {
    await self.asyncMethod()
    // 完了後の処理
}

下のようにasyncMethodの戻り値を受け取る事もできます。

func asyncMethod() async -> String {
    return "OK"
}

Task {
    let result = await self.asyncMethod()
    print(result)
}

実行されるスレッドですが、Task内は元のスレッドとは別スレッドで実行されます。
具体的には下のようになります。

Task内のawaitまでの処理は同じスレッドで行われるという記述もあったのですが、検証したときは別スレッドになっていました。
この辺りは機会あれば詳しく調べてみようと思います。

func asyncMethod() async {
    print("asyncMethod: \(Thread.isMainThread)") // → false
}

print("1: \(Thread.isMainThread)") // → true
Task {
    print("2: \(Thread.isMainThread)") // → false
    await self.asyncMethod()
    print("3: \(Thread.isMainThread)") // → false
}

クラスに@MainActorを付けることでasyncのメソッドをメインスレッドで実行する事も可能です。

@MainActor
class Test {
    func test() {
        print("1: \(Thread.isMainThread)") // → true
        Task {
            print("2: \(Thread.isMainThread)") // → false
            await self.asyncMethod()
            print("3: \(Thread.isMainThread)") // → true
        }
    }
    
    func asyncMethod() async {
        print("asyncMethod: \(Thread.isMainThread)") // → true
    }
}

AdMobのAPI用のリフレッシュトークンを取得する

毎回忘れてしまうのでまとめました。
こちらは2022/08時点の情報なので最新情報は下URLをご参照下さい。

developers.google.com

まずはGoogle Cloud Platformからプロジェクトを作成します。

Google Cloud Platform

作成後は下URLからプロジェクトのAdMobのAPIを有効化します。

Google Cloud Platform

APIは下画面の「APIとサービスの有効化」から有効にできます。

次はAPIアクセスのためのリフレッシュトークンを取得します。
先程のページの「認証情報」の「CREATE CREDENTIALS」ボタンからOAuthクライアントIDを作成します。

アプリケーションの種類はデスクトップアプリを選択します。
名前は分かりやすい名前を付けます。

ここで取得したclient_idを使って下のURLにアクセスします。
アクセスするとAdMobのAPIへのアクセス許可を求められるので許可します。

https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/admob.readonly&access_type=offline&include_granted_scopes=true&redirect_uri=http://localhost:3000&response_type=code&client_id=[先程取得したclient_id]

許可をした後は下のURLに飛ばされます。
このURLのcodeの部分はリフレッシュトークン取得に使うのでメモしておきます。

http://localhost:3000/?code=XXXX&scope=https://www.googleapis.com/auth/admob.readonly%20https://www.googleapis.com/auth/adsense.readonly

今取得したcodeとclient_idとclient_secretを使って下URLにリクエストを送ります。

curl -d client_id=[先程取得したclient_id] -d client_secret=[先程取得したclient_secret] -d redirect_uri=http://localhost:3000 -d grant_type=authorization_code -d code=[先程メモしたcode] https://accounts.google.com/o/oauth2/token

レスポンスは下のとおりです。
refresh_tokenの部分にリフレッシュトークンが入ります。

{
  "access_token": "XXX",
  "expires_in": 3599,
  "refresh_token": "XXX",
  "scope": "https://www.googleapis.com/auth/admob.readonly https://www.googleapis.com/auth/adsense.readonly",
  "token_type": "Bearer"
}

【Kotlin】Androidで通知を送る

Androidの通知を実装してみました。

通知は下の手順で送信します。

  1. チャンネルの作成
  2. チャンネルの登録
  3. 通知の送信

チャンネルはAndroid 8.0から必須になったもので、通知のグループのようなものです。
ユーザーはチャンネルごとに通知をOFFにしたりする事ができます。

チャンネルは下のように生成します。

val channelId = "channelId"
val channel = NotificationChannel(channelId, "チャンネルの名前", NotificationManager.IMPORTANCE_DEFAULT).apply {
    description = "チャンネルの説明文"
}

生成したチャンネルはNotificationManagerを使って登録します。
一度登録すれば今後は登録しなくても通知を送る事ができます。

val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
manager?.createNotificationChannel(channel)

登録したチャンネルの名前や説明文下のように表示されます。

チャンネルが複数ある場合、ユーザーの管理画面には下のように表示されます。

通知の送信処理は下の通りです。
NotificationCompat.Builderの第1引数にはContext(今回はActivity内なのでthisを使用)で第2引数には先程登録したチャンネルのIDを入れます。

val builder = NotificationCompat.Builder(this, "channelId")
    .setSmallIcon(androidx.core.R.drawable.notification_bg_normal)
    .setContentTitle("通知のタイトル")
    .setContentText("通知本文")
NotificationManagerCompat.from(this).apply {
    notify(0, builder.build())
}

送った通知は下のように表示されます。

もし登録していないチャンネルIDを使った場合は下のような警告が出ます。
見た所、開発中のみ表示されるものかと思います。
この時、アプリは落ちないのですが通知も送られません。

通知ですがsetSmallIconだけは必ず付ける必要があります。
これがない場合は下のエラーが出てアプリが落ちます。

java.lang.IllegalArgumentException: Invalid notification (no valid small icon):

チャンネルはNotificationChannelの第3引数のImportanceを変える事で音を鳴らさない事などもできます。
また、音自体も変更する事が可能です。

登録したチャンネル一覧は下のように取得する事ができます。

val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
manager?.notificationChannels?.forEach {
    Log.e("test", "${it.id}")
}

また、登録した通知は下のように削除する事ができます。

val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
manager?.deleteNotificationChannel(channelId)

参考URL

通知を作成する  |  Android デベロッパー  |  Android Developers

[kotlin] コピペでandroidに通知を出す - Qiita

Kotlinでサービスを使ってみる

Androidのサービスという仕組みを試してみました。
サービスとはバックグラウンドで処理を行う時などに使う機能で、ActivityからUIを除いたような動作をします。

まずはプロジェクトを作成してからServiceを追加します。

名前などを適当につけて作成します。

作成すると下のようなクラスが作られます。

class MyService : Service() {

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
}

AndroidManifest.xmlには下のように追加されます。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication">
    <application>

        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>

    </application>
</manifest>

次はサービスを実行してみます。
実行前にログを出力する処理だけ追加します。

class MyService : Service() {
    override fun onCreate() {
        super.onCreate()

        Log.d("test", "MyService onCreate")
    }
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("test", "MyService onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }
    
    override fun onDestroy() {
        super.onDestroy()

        Log.d("test", "MyService onDestroy")
    }
}

MainActivityから下のように呼び出しました。

startService(Intent(this, MyService::class.java))

無事にログが表示されました。
サービスで実行したい処理などはonStartCommandに記述します。
onStartCommandの戻り値はサービス強制終了時の挙動方法を返します。

次はサービスを終了してみます。
終了はActivityでstopServiceメソッドを呼び出すだけです。
引数には終了したいServiceのIntentを入れます。
引数のIntentはstartで渡したインスタンスと違うものでも構いません。

stopService(Intent(this, MyService::class.java))

Service側でstopSelfを呼び出す事でも終了できます。

stopSelf()

次はサービスにバインドして利用してみます。
そうする事でActivityからサービスを操作したり値を受け取ったりする事ができるようになります。
startServiceと違ってバックグラウンドで動き続けるわけではないので注意が必要です。
ずっと動かしたいはstartServiceと併用する必要があります。

まずはMyServiceを下のように修正します。
IncomingHandlerはActivityとやり取りするためのクラスです。
bindした時はonStartCommandは呼ばれず、代わりにonBindが呼ばれます。

class MyService : Service() {
    lateinit var messenger: Messenger

    override fun onBind(intent: Intent?): IBinder? {
        messenger = Messenger(IncomingHandler(this))
        return messenger.binder
    }

    internal class IncomingHandler(context: Context, private val applicationContext: Context = context.applicationContext) : Handler() {
        override fun handleMessage(msg: Message) {
            Log.d("test", "IncomingHandler")
        }
    }
}

次はActivity側も下のように修正します。
Serviceとの連携に必要なconnectionというインスタンス変数を作成します。

class MainActivity : AppCompatActivity() {
    private var messender: Messenger? = null
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            messender = Messenger(service)
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
        }
    }
}

Serviceの起動はActivityでbindServiceを呼び出します。
第2引数に先程作ったconnectionを使います。

bindService(intent, connection, Context.BIND_AUTO_CREATE)

bind後にactivityからメッセージを送る方法は下の通りです。
これによってIncomingHandlerのhandleMessageが呼ばれます。

messender?.send(Message.obtain(null, 0))

参考URL

サービスの概要  |  Android デベロッパー  |  Android Developers

【Kotlin研修10日目】バックグラウンド処理の実装、サービスのライフサイクル - Qiita