20200216のAndroidに関する記事は5件です。

[Flutter] Dribbbleデザイン、そのまま作ろう その14

挨拶

こんにちは 皆さん!Dreamwalkerです。
明けましておめでとうございます。
令和2年の初投稿になります。

ほぼ2ヶ月ぶりに書き始めることになってしまいました。泣

:fire:今回のデザイン:fire:

今回は14番目の「DribbbleのデザインをFlutterでやってみた」になります。

Monochrome Shop App

image.png

https://dribbble.com/shots/9954517-Monochrome-Shop-App/attachments/1989500?mode=media

:clap:結果:clap:

一つ目のページ。

0_Screenshot_1581338497.png

二つ目のページ。

1_Screenshot_1581338494.png

三つ目のページ。

1_Screenshot_1581338494.png

:fire:必要なライブラリー:fire:

1.flutter_staggered_grid_view: ^0.3.0

:clap:全てのコード

main。

main.dart
import 'package:flutter/material.dart';
void main() => runApp(MonochromeShopApp());

MaterialApp Importer。

home_page.dart
class MonochromeShopApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MonochromeShopHome(),
    );
  }
}

一つ目のページ。

個人的にお気に入りのデザインになります。
AppBarのタイトルはイメージがないので、Widgetで作ってみました。

ShopMainPage

main_page.dart
class MonochromeShopHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Positioned(
            top: 34,
            left: 16,
            right: 16,
            child: Container(
              height: 100,
              child: Stack(
                children: <Widget>[
                  Center(
                    child: Text(
                      "LOSER",
                      style: TextStyle(
                        fontSize: 64,
                        letterSpacing: -6,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  Center(
                    child: Text(
                      "V",
                      style: TextStyle(
                        fontSize: 84,
                        letterSpacing: -8,
                        color: Colors.red,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  )
                ],
              ),
            ),
          ),
          Positioned(
            bottom: 32,
            right: 24,
            child: GestureDetector(
              onTap: () {
                Navigator.of(context).push(MaterialPageRoute(
                    builder: (context) => WomensBlazersPage()));
              },
              child: Container(
                height: 86,
                width: 86,
                decoration:
                    BoxDecoration(shape: BoxShape.circle, border: Border.all()),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      "explore",
                      style: TextStyle(fontSize: 16),
                    ),
                    SizedBox(
                      height: 4,
                    ),
                    Icon(
                      Icons.arrow_forward,
                      size: 20,
                    ),
                  ],
                ),
              ),
            ),
          ),
          Positioned(
            left: 4,
            bottom: 4,
            child: Container(
              width: MediaQuery.of(context).size.width / 2,
              height: MediaQuery.of(context).size.height / 3,
              decoration: BoxDecoration(
                  color: Colors.black,
                  image: DecorationImage(
                      image: NetworkImage(
                          "https://cdn.pixabay.com/photo/2014/01/04/14/35/fashion-238553__340.jpg"),
                      fit: BoxFit.cover)),
            ),
          ),
          Positioned(
            right: 0,
            bottom: MediaQuery.of(context).size.height / 3.5,
            child: Container(
              width: MediaQuery.of(context).size.width / 1.4,
              height: MediaQuery.of(context).size.height / 2.8,
              decoration: BoxDecoration(
                  color: Colors.black,
                  image: DecorationImage(
                    image: NetworkImage(
                        "https://cdn.pixabay.com/photo/2016/06/17/09/54/beauty-1462986__340.jpg"),
                    fit: BoxFit.cover,
                  ),
                  border: Border.all(color: Colors.white, width: 5)),
            ),
          ),
          Positioned(
            left: 0,
            top: 136,
            child: Container(
              width: MediaQuery.of(context).size.width / 2.4,
              height: MediaQuery.of(context).size.height / 3.4,
              decoration: BoxDecoration(
                  color: Colors.black,
                  border: Border.all(color: Colors.white, width: 5),
                  image: DecorationImage(
                      colorFilter:
                          ColorFilter.mode(Colors.black, BlendMode.color),
                      image: NetworkImage(
                          "https://cdn.pixabay.com/photo/2016/03/09/10/22/two-women-1246024__340.jpg"),
                      fit: BoxFit.cover)),
            ),
          ),
          Positioned(
            left: MediaQuery.of(context).size.width / 2.4 + 16,
            top: 136,
            right: 0,
            child: Container(
              height: 120,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  Text(
                    "Men's",
                    style: TextStyle(fontSize: 18),
                  ),
                  Text(
                    "Blazers",
                    style: TextStyle(fontSize: 18),
                  )
                ],
              ),
            ),
          ),
          Positioned(
            left: 16,
            top: MediaQuery.of(context).size.height / 2,
            child: Container(
              height: 100,
              width: 100,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Text(
                    "Women's",
                    style: TextStyle(fontSize: 18),
                  ),
                  Text(
                    "Blazers",
                    style: TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
          ),
          Positioned(
            right: 16,
            left: MediaQuery.of(context).size.width / 2 + 24,
            bottom: 120,
            child: Container(
              height: 80,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  Text(
                    "Kid's",
                    style: TextStyle(fontSize: 18),
                  ),
                  Text(
                    "Blazers",
                    style: TextStyle(fontSize: 18),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

二つ目のページ。

GridViewが魅力的で今回のポイントになります。

entered_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_notebook_4th/ep355_monochrome_shop_app/detail_page.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

class Wear {
  String img;
  String price;
  String name;

  Wear({this.img, this.price, this.name});
}

List<Wear> items = [
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2017/08/06/09/51/blazer-2590798__340.jpg",
      price: "134",
      name: "Retro Blazer"),
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2017/08/10/08/00/suit-2619784__340.jpg",
      price: "365",
      name: "Fashion Blazer"),
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2016/07/19/10/48/girl-1527959__340.jpg",
      price: "428",
      name: "Classic Blazer"),
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2016/03/23/08/34/beautiful-1274361__340.jpg",
      price: "122",
      name: "Modern Blazer"),
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2017/08/06/09/51/blazer-2590798__340.jpg",
      price: "134",
      name: "Retro Blazer"),
  Wear(
      img:
          "https://cdn.pixabay.com/photo/2017/08/06/09/51/blazer-2590798__340.jpg",
      price: "134",
      name: "Retro Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/07/19/10/48/girl-1527959__340.jpg",
      price: "428",
      name: "Classic Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/03/23/08/34/beautiful-1274361__340.jpg",
      price: "122",
      name: "Modern Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/07/19/10/48/girl-1527959__340.jpg",
      price: "428",
      name: "Classic Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/03/23/08/34/beautiful-1274361__340.jpg",
      price: "122",
      name: "Modern Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/07/19/10/48/girl-1527959__340.jpg",
      price: "428",
      name: "Classic Blazer"),
  Wear(
      img:
      "https://cdn.pixabay.com/photo/2016/03/23/08/34/beautiful-1274361__340.jpg",
      price: "122",
      name: "Modern Blazer"),
];

class WomensBlazersPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: <Widget>[
            Expanded(
              flex: 2,
              child: Padding(
                padding: const EdgeInsets.only(right: 16, left: 16),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Icon(
                      Icons.arrow_back,
                      color: Colors.grey.shade500,
                    ),
                    Text(
                      "Women's Blazers",
                      style: TextStyle(fontSize: 18, color: Colors.black87),
                    ),
                    Icon(
                      Icons.search,
                      color: Colors.grey.shade500,
                    ),
                  ],
                ),
              ),
            ),
            Expanded(
              flex: 13,
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: StaggeredGridView.countBuilder(
                  itemBuilder: (context, index) {
                    return GestureDetector(
                      onTap: (){
                        Navigator.push(context, MaterialPageRoute(
                          builder: (context)=>ItemDetailPage(
                            wear: items[index],
                          )
                        ));
                      },
                      child: Container(
                        child: Column(
                          children: <Widget>[
                            Expanded(
                              flex: 8,
                              child: Container(
                                decoration: BoxDecoration(
                                    image: DecorationImage(
                                        image: NetworkImage(items[index].img),
                                        fit: BoxFit.cover,
                                        colorFilter: ColorFilter.mode(
                                            Colors.black, BlendMode.color))),
                              ),
                            ),
                            Expanded(
                              flex: 2,
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                children: <Widget>[
                                  Text("\$ ${items[index].price}"),
                                  Text("${items[index].name}")
                                ],
                              ),
                            )
                          ],
                        ),
                      ),
                    );
                  },
                  itemCount: items.length,
                  crossAxisCount: 4,
                  mainAxisSpacing: 16,
                  crossAxisSpacing: 16,
                  staggeredTileBuilder: (int index) {
                    return new StaggeredTile.count(2, index.isEven ? 4 : 3);
                  },
                ),
//                child: GridView.builder(
//                  itemCount:items.length,
//                  itemBuilder: (BuildContext context, int index) {
//                    return Container(
//                      child: Column(
//                        children: <Widget>[
//                          Expanded(
//                            flex: 8,
//                            child: Container(
//                              decoration: BoxDecoration(
//                                image: DecorationImage(
//                                  image: NetworkImage(
//                                    items[index].img
//                                  ),
//                                  fit: BoxFit.cover,
//                                  colorFilter: ColorFilter.mode(Colors.black, BlendMode.color)
//                                )
//                              ),
//                            ),
//                          ),
//                          Expanded(
//                            flex: 2,
//                            child: Row(
//                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
//                              children: <Widget>[
//                                Text("\$ ${items[index].price}"),
//                                Text("${items[index].name}")
//                              ],
//                            ),
//                          )
//                        ],
//                      ),
//                    ) ;
//                  },
//                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//                    crossAxisCount: 2,
//                    childAspectRatio: 0.55,
//                    crossAxisSpacing: 16,
//                    mainAxisSpacing: 16
//                  ),
//
//                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

class searcher extends SearchDelegate {
  @override
  List<Widget> buildActions(BuildContext context) {
    // TODO: implement buildActions
    throw UnimplementedError();
  }

  @override
  Widget buildLeading(BuildContext context) {
    // TODO: implement buildLeading
    throw UnimplementedError();
  }

  @override
  Widget buildResults(BuildContext context) {
    // TODO: implement buildResults
    throw UnimplementedError();
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    // TODO: implement buildSuggestions
    throw UnimplementedError();
  }
}



三つ目のページ。

Product Detail Page

detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_notebook_4th/ep355_monochrome_shop_app/entered_page.dart';

class ItemDetailPage extends StatelessWidget {
  final Wear wear;

  ItemDetailPage({this.wear});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Expanded(
              flex: 2,
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 24),
                child: Row(
                  children: <Widget>[
                    IconButton(
                      icon: Icon(Icons.clear),
                      onPressed: () {
                        Navigator.pop(context);
                      },
                    ),
                    Spacer(),
                    Text(
                      "${wear.name}",
                      style: TextStyle(fontSize: 20),
                    ),
                    Spacer(),
                  ],
                ),
              ),
            ),
            Expanded(
              flex: 6,
              child: Container(
                decoration: BoxDecoration(
                  image: DecorationImage(
                      image: NetworkImage(wear.img),
                      fit: BoxFit.cover,
                      colorFilter:
                          ColorFilter.mode(Colors.black, BlendMode.color)),
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: Padding(
                padding: const EdgeInsets.only(left: 24),
                child: Row(
                  children: <Widget>[
                    Container(
                      margin: EdgeInsets.only(right: 12),
                      height: 4,
                      width: 24,
                      decoration: BoxDecoration(
                        color: Colors.blueGrey,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(right: 12),
                      height: 4,
                      width: 24,
                      decoration: BoxDecoration(
                        color: Colors.grey,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(right: 12),
                      height: 4,
                      width: 24,
                      decoration: BoxDecoration(
                        color: Colors.grey,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(right: 12),
                      height: 4,
                      width: 24,
                      decoration: BoxDecoration(
                        color: Colors.grey,
                      ),
                    )
                  ],
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 24),
                child: Text("No matter the occasion girl, get ready to edge\n"
                    "out your suits collection and be best dressed"),
              ),
            ),
            Expanded(
              flex: 7,
              child: Column(
                children: <Widget>[
                  Expanded(
                    flex: 1,
                    child: Container(
                      child: Row(
                        children: <Widget>[
                          Expanded(
                            flex: 10,
                            child: Container(
                              padding: EdgeInsets.only(left: 40),
                              height: double.infinity,
                              color: Colors.grey.shade200,
                              child: Align(
                                  alignment: Alignment.centerLeft,
                                  child: Text(
                                    "Detail",
                                    style: TextStyle(fontSize: 20),
                                  )),
                            ),
                          ),
                          Expanded(
                            flex: 2,
                            child: Container(
                              child: Center(
                                child: Icon(
                                  Icons.card_giftcard,
                                  color: Colors.grey.shade400,
                                ),
                              ),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(
                      child: Row(
                        children: <Widget>[
                          Expanded(
                            flex: 10,
                            child: Container(
                              padding: EdgeInsets.only(left: 80),
                              height: double.infinity,
                              color: Colors.grey.shade400,
                              child: Align(
                                  alignment: Alignment.centerLeft,
                                  child: Text(
                                    "Delivery",
                                    style: TextStyle(fontSize: 20),
                                  )),
                            ),
                          ),
                          Expanded(
                            flex: 2,
                            child: Container(
                              child: Center(
                                child: Icon(
                                  Icons.favorite_border,
                                  color: Colors.grey.shade400,
                                ),
                              ),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(
                      child: Row(
                        children: <Widget>[
                          Expanded(
                            flex: 10,
                            child: Container(
                              padding: EdgeInsets.only(left: 120),
                              height: double.infinity,
                              color: Colors.grey.shade600,
                              child: Align(
                                  alignment: Alignment.centerLeft,
                                  child: Text(
                                    "Discount",
                                    style: TextStyle(fontSize: 20),
                                  )),
                            ),
                          ),
                          Expanded(
                            flex: 2,
                            child: Container(
                              child: Center(
                                child: Icon(
                                  Icons.share,
                                  color: Colors.grey.shade400,
                                ),
                              ),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(
                      child: Row(
                        children: <Widget>[
                          Expanded(
                            flex: 10,
                            child: Container(
                              child: Row(
                                children: <Widget>[
                                  Expanded(
                                    flex: 3,
                                    child: Container(
                                      color: Colors.white,
                                      child: Center(
                                        child: Text(
                                          "\$${wear.price}",
                                          style: TextStyle(
                                            fontSize: 20,
                                            fontWeight: FontWeight.bold,
                                          ),
                                        ),
                                      ),
                                    ),
                                  ),
                                  Expanded(
                                    flex: 8,
                                    child: Container(
                                      color: Colors.black,
                                      child: Center(
                                        child: Text(
                                          "Add to Cart",
                                          style: TextStyle(
                                              color: Colors.white,
                                              fontSize: 20),
                                        ),
                                      ),
                                    ),
                                  )
                                ],
                              ),
                            ),
                          ),
                          Expanded(
                            flex: 2,
                            child: Container(
                              child: Center(
                                child: Icon(
                                  Icons.more_horiz,
                                  color: Colors.grey.shade400,
                                ),
                              ),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}


:clap:終わりに

今回も読んでくださってありがとうございます。:bow_tone2:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Emulatorでnet::ERR_CLEARTEXT_NOT_PERMITTEDが出た時の対処法

WebViewでlocalhostを参照する実装をAndroid Emulatorで見る際、net::ERR_CLEARTEXT_NOT_PERMITTEDというエラーが表示された。
スクリーンショット 2020-02-16 18.47.53_.png

これはAndroid 9(API レベル 28)からTLS(Transport Layer Security)がデフォルトとなり、HTTPプロトコル通信許が可されなくなったことが原因。
Google Developers Japan: Android P で TLS のデフォルト化によるユーザー保護

対処法はいくつかあるが、今回はサンプルアプリということもあり、マニフェストファイルにHTTP通信を許可するようusesCleartextTrafficの設定を追加することで回避した。

AndroidManifest.xml
<application
    ...
    android:usesCleartextTraffic="true"
    ...
>
</application>

厳密に対応する場合は、特定ドメインのみを許可する設定などが推奨されている。

AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest ... >
        <application android:networkSecurityConfig="@xml/network_security_config"
                        ... >
            ...
        </application>
    </manifest>
res/xml/network_security_config.xml
    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config>
            <domain includeSubdomains="true">secure.example.com</domain>
            <domain includeSubdomains="true">cdn.example.com</domain>
            <trust-anchors>
                <certificates src="@raw/trusted_roots"/>
            </trust-anchors>
        </domain-config>
    </network-security-config>

ネットワーク セキュリティ構成  |  Android デベロッパー  |  Android Developers

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Emulatorでnet::ERR_CONNECTION_REFUSEDが出た時の対処法

WebViewでlocalhostを参照する実装をAndroid Emulatorで見る際、net::ERR_CONNECTION_REFUSEDというエラーが表示された。
スクリーンショット 2020-02-16 18.45.52_.png

接続先はhttp://localhost:3000/を指定していたのだが、Android Emulatorは仮想環境上で実行されているため、ホストマシンで直接実行しているローカルサーバーには接続できない模様。

エミュレータの各インスタンスは、仮想ルーターおよびファイアウォール サービスの背後で実行されます。この仮想ルーターおよびファイアウォール サービスによって、各インスタンスは開発マシンのネットワーク インターフェースと設定、そしてインターネットから分離されます。エミュレートしたデバイスでは、開発マシンやネットワーク上のその他のエミュレータ インスタンスは認識されず、イーサネット経由でルーター / ファイアウォールに接続されていることだけが認識されます。

各インスタンスの仮想ルーターは、10.0.2/24 ネットワーク アドレス空間を管理します。ルーターによって管理されるすべてのアドレスは、10.0.2.xx の形式を持ちます(xx は数字)。この空間内のアドレスは、エミュレータ / ルーターによって次のように事前に割り当てられます。

Android Emulator のネットワークを設定する  |  Android デベロッパー  |  Android Developers

Android Emulatorからホストマシンのローカルループバックアドレス(127.0.0.1)を参照するためには、ネットワークアドレス10.0.2.2を使用する必要がある。

ネットワーク アドレス 説明
10.0.2.1 ルーター / ゲートウェイ アドレス
10.0.2.2 ホスト ループバック インターフェースへの特殊エイリアス(開発マシンの 127.0.0.1 など)
10.0.2.3 1 番目の DNS サーバー
10.0.2.4 / 10.0.2.5 / 10.0.2.6 オプションの 2 番目、3 番目、4 番目の DNS サーバー(存在する場合)
10.0.2.15 エミュレートしたデバイスのネットワーク / イーサネット インターフェース
127.0.0.1 エミュレートしたデバイスのループバック インターフェース

WebViewの接続先をhttp://10.0.2.2:3000/に指定することで、localhostへアクセスすることができた。
スクリーンショット 2020-02-16 18.32.11_.png

これまでAndroid Emulator上でlocalhostを参照する機会がなかったため、大変勉強になった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ProcessLifeCycleOwnerを使ってみて

はじめに

アプリ復帰時の処理をonResumeで行うと通常のActivity遷移でも反応してしまうので困っていました。
(諸般の事情でAcitivityを多用するアプリだったので)

isOnCreate のようなフラグ管理やIntent呼び出し時にフラグをつけるなど考えましたが、
いまいちスマートではなく...
そんな時、この記事を発見して、参考に取り組みました。

アプリのバックグラウンド⇆フォアグラウンドを検知する。ProcessLifecycleOwnerの導入
ProcessLifecycleOwnerでアプリのバックグラウンド移行とフォアグラウンド復帰を検知する

正直、前出の記事の焼き増しか劣化版になりますが、自分用の備忘録に残します。

ケーススタディ

ソースは こちら です

今回は、ProcessLifeCycleOwner + Event Bus で実装してみました。
(EventBusは便利で導入も容易ですが、若干古いかもしれないですね...)

ソースの主要な部分を以下に掲載します。

App.kt
class App : Application(), LifecycleObserver {

    companion object {
        const val TAG = "App"
    }

    override fun onCreate() {
        super.onCreate()

        ProcessLifecycleOwner.get().lifecycle.addObserver(this)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreateLifecycle() {
        // 最初の1回のみ
        Log.d(TAG, "ON_CREATE")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStartLifecycle() {
        // アプリ開始時 or バックグラウンドからの復帰時
        Log.d(TAG, "ON_START")

        EventBus.getDefault().post(AppForegroundMessage())
    }
/// 以下、省略

}
BaseActivity.kt
abstract class BaseActivity : AppCompatActivity() {

    private val forceVersionUpLogic =
        ForceVersionUpLogic()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onStart() {
        super.onStart()
        EventBus.getDefault().register(this)
    }

    override fun onResume() {
        super.onResume()
    }

    override fun onStop() {
        super.onStop()
        EventBus.getDefault().unregister(this)
    }

    @Subscribe
    fun onReceiveMessage(eventMessage: AppForegroundMessage) {
        ApiManager.service.getVersionJson().enqueue(object :
            Callback<VersionModel> {
            override fun onFailure(call: Call<VersionModel>, t: Throwable) {
                Log.d(
                    "AppForegroundMessage",
                    "ApiManager.service.getVersionJson() onFailure",
                    t
                )
            }

            override fun onResponse(call: Call<VersionModel>, response: Response<VersionModel>) {

                response.body()?.let {
                    if (forceVersionUpLogic.checkForceVersionUp(it.version,
                            BuildConfig.VERSION_NAME
                        )) {
                        AlertDialog.Builder(this@BaseActivity)
                            .setTitle("アップデート")
                            .setMessage("アプリストアで最新のアプリをインストールしてください")
                            .setPositiveButton("ストアへ") { _, _ ->
                                // https://developer.android.com/distribute/marketing-tools/linking-to-google-play?hl=ja
                                val intent = Intent(
                                    Intent.ACTION_VIEW
                                ).apply {
                                    data =
                                        Uri.parse("https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}")
                                }
                                startActivity(intent)
                            }
                            .show()
                    }
                }
            }
        })
    }
}

設計としては単純ですが、アプリ起動・フォアグラウンド復帰時はダイアログが表示されました。
MainActivity <-> SubActivity の Activity遷移時はダイアログは表示されませんでした。

MVVMなどの組み合わせが要検証ですが、Activityを複数使う場合の手段になると思います。

まとめ

ProcessLifeCycleOwnerを使うことで、
ActivityのOnResumeでのアプリ復帰処理ではないところでイベントのハンドリングができるようになります。
アプリ起動時に通信をする場合などはちょっとタイミングの調整が必要そうですが、
今後はこちらで実装を進めるのが良いと思います。

追伸
最近、見つけたのですが、こちらも良記事です。
【Android】ライフサイクル(Lifecycle)アンチパターン 〜2020年版〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutterアプリのライフサイクル

はじめに

Flutterアプリのライフサイクルについてまとめています。Flutterのライフサイクルというと、

  • アプリ (AppLifecycleState) ← 今回の内容
  • 画面 (StatefulWidget)

の2種類がありますが、今回は上のアプリ自体のライフサイクルについての内容です。

StatefulWidgetのライフサイクルについては、Flutter StatefulWidgetのライフサイクルにまとめていますので、参照してください。

アプリのライフサイクル一覧とプラットフォームの対応関係

AppLifecycleStateの状態遷移と状態一覧を以下に示します。
スクリーンショット 2020-02-17 20.02.01.png

基本的にプラットフォーム側の状態をFlutterアプリの状態として再定義している感じですが、iOSのライフサイクルに近いと思います。なお、iOSのライフサイクルは詳しくなく、AndroidエンジニアのためのiOSのUIViewControllerのライフサイクルとAndroidのActivityのライフサイクル比較を参考にさせていただきました。間違いがあればコメントでご指摘いただけると幸いです。

inactivepausedの違いは、画面が表示されているか否かの違いの様子です。

状態 内容 Android iOS
inactive アプリは表示されているが、フォーカスがあたっていない状態 onStart, onPause viewDidLoad
paused アプリがバックグラウンドに遷移し(最前面に表示されてない)、入力不可な一時停止状態 onPause viewWillDisappear viewDidDisappear
resumed アプリがフォアグランドに遷移し(paused状態から復帰)、復帰処理用の状態 onResume viewWillAppear viewDidAppear
detached アプリが終了する時に通る終了処理用の状態 onDestroy dealloc

AndroidのonSaveInstanceStateの様な仕組みはあるのか?

現状なさそうです。
AppLifecycleStateの状態を見て、必要であれば自分で対応する必要があります。

状態遷移ユースケース

いくつかのユースケースで状態遷移の動作を確認してみました。

WidgetsBindingObserverを利用するとAppLifecycleStateの状態が取得出来ます。
その他、SystemChannels.lifecycle (詳細はこちらにまとめてます。) を利用しても取得可能です。

確認のためのソースコード

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

  AppLifecycleState _state;

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

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

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print('state = $state');
  }

アプリ起動

イベント発生なし

画面回転

イベント発生なし

アプリ終了

I/flutter (13730): state = AppLifecycleState.inactive
I/flutter (13730): state = AppLifecycleState.paused
I/flutter (13730): state = AppLifecycleState.detached

ホームボタンを押す

I/flutter (13730): state = AppLifecycleState.inactive
I/flutter (13730): state = AppLifecycleState.paused

画面OFF

I/flutter (15366): state = AppLifecycleState.inactive
I/flutter (15366): state = AppLifecycleState.paused

アプリを履歴から復帰

I/flutter (13730): state = AppLifecycleState.resumed

画面分割で起動

I/flutter (13252): state = AppLifecycleState.inactive

画面分割状態でサブ画面 (下画面) のアプリを終了

I/flutter (13252): state = AppLifecycleState.inactive

画面分割状態でサブ画面 (下画面) で新しいアプリを洗濯して、画面を2分割に戻る

I/flutter (13252): state = AppLifecycleState.resumed

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む