20200817のAndroidに関する記事は11件です。

kotlinとjavaでリストを作りたい!

はじめに…

この記事は2個前の記事で書いた、

7日間毎日記事投稿の3日目

になってます

使うコードは下に貼りますが、このアプリの詳しい機能はその2個前の記事をご覧ください!

↓ここからが今回の記事の本題です↓

それぞれの言語でリストを扱うには…

-- 目次 --

  1. Arraylistの作り方

  2. 要素を追加・削除する方法

  3. 要素数を取得する方法

  4. リストの中身を逆順・シャッフルする方法

  5. 中身を違うリストにコピーする方法

ArrayListの作り方

  • javaの場合
ArrayList<要素の型>変数名 = new ArrayList<>();

例)

WhoActivity.java
ArrayList<String>memberL = new ArrayList<>();  // 20行目

変数定義と大きく異なっている

  • kotlinの場合
val/var 変数名:ArrayList<要素の型> = arrayListOf()

例)

WhoActivity.kt
var memberL:ArrayList<String> = arrayListOf()  // 13行目

リストの中身の指定方法以外は変数定義とよく似ている

要素の型を指定する方法は両者ともよく似ている

要素を追加・削除する方法

  • javaの場合
// 追加
リスト名.add(要素);
// 削除
リスト名.remove(要素の場所);

例)

WhoActivity.java
//追加
memberL.add(memberET.getText().toString());  // 82行目
ResultActivity.java
// 削除
memberL.remove(0);  // 46行目
  • kotlinの場合
// 追加
リスト名.add(要素)
// 削除
リスト名.remove(要素)

例)

WhoActivity.kt
// 追加
memberL.add(member_et.text.toString())  // 54行目
ResultActivity.kt
// 削除
memberL.remove(memberL[0])  // 37行目

追加の仕方は同じだが、削除するときは対象の要素を指定するために使用する物が違う

要素数を取得する方法

  • javaの場合
リスト名.size();

例)

resultActivity.java
int memberNum = memberL.size();  // 33行目
  • kotlinの場合
リスト名.size

例)

ResultActivity.kt
val memberNum = memberL.size  // 24行目

javaは.size()、kotlinは.sizeとよく似ている

リストの中身を逆順・シャッフルする方法

  • javaの場合
// 逆順
Collections.reverse(リスト名);
// シャッフル
Collections.shuffle(リスト名);

例)

WhoActivity.java
// 逆順
ArrayList<String> memberLR = (ArrayList<String>) memberL.clone();  //84行目
Collections.reverse(memberLR);

※ 1行目の処理内容は「中身を違うリストにコピーする方法」参照

resultActivity.java
// シャッフル
Collections.shuffle(memberL);  // 34行目
  • kotlinの場合
// 逆順
リスト名.reverse()
// シャッフル
リスト名.shuffle(Random())

例)

WhoActivity.kt
// 逆順
var memverLR = ArrayList<String>(memberL)  // 56行目
memverLR.reverse()

※ 1行目の処理内容は「中身を違うリストにコピーする方法」参照

ResultActivity.kt
// シャッフル
memberL.shuffle(Random())  // 25行目

書き方は大きく違うがどちらも
逆順の場合は「reverse」、シャッフルの場合は「shuffle」というのがキーワードとなっている

中身を違うリストにコピーする方法

  • javaの場合
ArrayList<要素の型> 変数名 = (ArrayList<要素の型>) コピーするリスト名.clone();

例)

WhoActivity.java
ArrayList<String> memberLR = (ArrayList<String>) memberL.clone();  // 84行目
  • kotlinの場合
val/var 変数名 = ArrayList<要素の型>(コピするリスト名)

例)

WhoActivity.kt
var memverLR = ArrayList<String>(memberL)  // 56行目

javaは.clone()を使用し、kotlinはリスト定義の方法と少し書き方が変わる

最後に…

今回はjavaとkotlinでリストの定義をしました。
まだまだたくさんの機能があると思うので、またほかのコードで出てきた際に記事にできたらなと思います。

また、1日目に記載した予定から今日以降の予定を少し変更しました。
その記事の予定も変更しておくので、あまり関係ないですが
一応自分のメモ用としてもここに記載しておきます。

明日も記事を投稿するので、引き続き温かく見守ってください。

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

ask me! appのログイン画面の実装

1 この記事の内容

ask me! appでのログイン画面の実装方法について記載します。画面レイアウトは下記のとおりです。

quitta①+.jpg

2 ソースコード概要

ソースコードの概要は下記の通りです。

login.dart

  @override
  Widget build(BuildContext context) {
    return
      new GestureDetector(
      onTap:()
      {
        _focusNodePwd.unfocus();
      },
      child:

      new Scaffold(
      appBar: new AppBar(
      centerTitle: true,

        title: new Text('ask me! app',style: TextStyle(fontFamily: 'impact')),
        backgroundColor:Color.fromARGB(255,171 ,232 ,255 ),

      ),

      body:
      KeyboardActions(
      config:_buildConfig(context),
    child:

          _loading?
        bodyProgress:
        new Column(
            children: <Widget>[

              new Padding(
                padding:  EdgeInsets.fromLTRB(24.0*app_size.width/WIDTH,24.0*app_size.height/HEIGHT,24.0*app_size.width/WIDTH,24.0*app_size.height/HEIGHT),
              ),

              new Center(
                child:
                new Text(
                  "H E L L O",
                  style: new TextStyle(fontSize: 56.0*app_size.width*app_size.height/WIDTH/HEIGHT,
                      color: Color.fromARGB(255,171 ,232 ,255 ),
                      fontWeight: FontWeight.w300,
                      fontFamily: 'impact'),
                ),
              ),


              new Center(
                child:
                new Image.asset(
                  'icon/askme.png',
                  width: 230.0*app_size.width/WIDTH,
                  height: 230.0*app_size.width/WIDTH,
                ),
              ),


              new Padding(
                padding:  EdgeInsets.fromLTRB(20.0*app_size.width/WIDTH,10.0*app_size.height/HEIGHT,20.0*app_size.width/WIDTH,10.0*app_size.height/HEIGHT),
              ),


              new Container(
                  width: 300.0*app_size.width/WIDTH,
                  child:new EnsureVisibleWhenFocused(
                    focusNode: _focusNodePwd,
                    child: new TextFormField(
                      controller: passwdcontroller,
                      decoration: const InputDecoration(
                        border: const OutlineInputBorder(),
                        hintText: 'PASSWORD',
                        labelText: 'PASSWORD',
                        hintStyle: TextStyle(fontSize: 20.0,
                            color: const Color(0xFF000000),
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'),
                        labelStyle:  TextStyle(fontSize: 20.0,
                            color: const Color(0xFF000000),
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'),

                      ),
                      style: new TextStyle(fontSize: 20.0,
                          color: const Color(0xFF000000),
                          fontWeight: FontWeight.w200,
                          fontFamily: 'Roboto'),
                      focusNode: _focusNodePwd,
                      keyboardType: TextInputType.number,
                    ),
                  ),
              ),

              new Padding(
                padding:  EdgeInsets.fromLTRB(20.0*app_size.width/WIDTH,20.0*app_size.height/HEIGHT,20.0*app_size.width/WIDTH,20.0*app_size.height/HEIGHT),
              ),


              new Row(
                  children: <Widget>[

                    new Padding(
                      padding:  EdgeInsets.fromLTRB(app_size.width*0.1,40.0*app_size.height/HEIGHT,0.1*app_size.width,40.0*app_size.height/HEIGHT),
                    ),


                  new Container(

                    width:app_size.width*3/5,
                    child:
                    FlatButton(
                      onPressed: () {

                        buttonPressed();
                      },
                      color: Color.fromARGB(255,171 ,232 ,255 ),
                      child: Text(
                        "    OK    ",
                        style: new TextStyle(fontSize: 50.0,
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'
                            ,color: Colors.white
                        ),
                      ),
                      padding: EdgeInsets.symmetric(vertical: 0*app_size.height, horizontal: 0.05*app_size.width),
                     shape:  StadiumBorder()
                    ),
                  ),

                    new Padding(
                      padding:  EdgeInsets.fromLTRB(app_size.width*0.1,40.0*app_size.height/HEIGHT,0.1*app_size.width,40.0*app_size.height/HEIGHT),
                    ),

                  ]
              ),
            ]

        ),
      ),

        bottomNavigationBar: new BottomNavigationBar(
          items: [

            new BottomNavigationBarItem(
              icon: const Icon(Icons.arrow_back),
              title: new Text('back',style: TextStyle(fontFamily: 'impact'),),
            ),


          ],
          onTap:(int index){
            Navigator.of(context).pop();
          },
        ),
      ),
      );
  }

工夫点について個別に説明します。

3 端末のサイズ取得

端末の種類によってレイアウトが変わってしまうのを防ぐために、Container等のwidthには予め取得されたデバイスのサイズを利用します。

サイズ取得に失敗する場合もあるので、その時は再度get_size()を呼び出すようにしています。

ソースコードは下記の通りです。

login.dart
  void get_size(){
    app_size=MediaQuery.of(context).size;
    setState(() {
      width = app_size.width;
      height = app_size.height;
    });

    if(width==0 || height==0) get_size();
  }

  @override
  void initState()  {
    super.initState();
    get_size();
  }

参考サイト:https://qiita.com/najeira/items/c98c5fec9c71104f8263

4 パスワード入力欄 のフォーカス外し

パスワードを入力するために、TextFormFieldをタップするとキーパッドが出ますが、
TextFormField外の箇所をタップするとキーパッドを閉じるようにしました。

実装方法としては、下記の通りです。

login.dart
      new GestureDetector(
      onTap:()
      {
        _focusNodePwd.unfocus();
      },

つまりOnTapイベントでパスワード入力欄に紐づけられたFocusNodeをUnFocusします。

5 TextFormFieldのヘルパーウィジェット

キーバッドが表示されたときに、TextFormFieldが隠れてしまう場合があります。
それを防ぐために下記サイトのヘルパーウィジェットを利用しました。

URL:https://www.didierboelens.com/2018/04/hint-4-ensure-a-textfield-or-textformfield-is-visible-in-the-viewport-when-has-the-focus/

6 キーパッドのDoneボタン

キーパッドでの入力完了後にはDoneボタンを押すことでキーパッドを閉じるようにしました。

Doneボタンの表示には下記のライブラリを使用しました。

Keyboard Actions:https://pub.dev/documentation/keyboard_actions/latest/

7 OKボタン押下時の動作

OKボタン押下時には、入力されたパスワードと予め設定された正解パスワードが合致するかどうか判定しますが、
その際の待ち時間は下記画面のようにCircularProgressIndicatorを使用します。

quitta_②.jpg

ソースコードは下記のとおりです。

login.dart
  var bodyProgress = new Container(
    child: new Stack(
      children: <Widget>[
        //body,
        new Container(
          alignment: AlignmentDirectional.center,
          decoration: new BoxDecoration(
            color: Colors.white70,
          ),
          child: new Container(
            decoration: new BoxDecoration(
                color: Colors.blue[200],
                borderRadius: new BorderRadius.circular(10.0)
            ),
            width: 300.0,
            height: 200.0,
            alignment: AlignmentDirectional.center,
            child: new Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Center(
                  child: new SizedBox(
                    height: 50.0,
                    width: 50.0,
                    child: new CircularProgressIndicator(
                      value: null,
                      strokeWidth: 7.0,
                    ),
                  ),
                ),
                new Container(
                  margin: const EdgeInsets.only(top: 25.0),
                  child: new Center(
                    child: new Text(
                      "please wait...",
                      style: new TextStyle(
                          color: Colors.white
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );



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

【Flutter, Dart】ミュータブルとイミュータブル、そしてfreezed

はじめに

本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。

元記事の公開は2019年7月15日です。
本文中に登場するコードは Dart で記述されています。

3点要点

  1. Mutableはダメです
  2. Immutableも正しく扱わないとデメリット大きいです
  3. コード生成(freezed)でImmutableの欠点を補いましょう

Mutableの代償とImmutableの代償

The Mutability Tax では、それぞれの設計によって生じるコードメンテナンスコストのことを Tax(税金) と形容しています。

本記事では 代償 と表現します。

Mutable(値が変更可能)

システム間でMutableなデータを渡す疎結合モジュールを持つシステムは、Mutability-Taxという名のメンテナンスコストを支払います。

? Mutableの代償①:予期せぬ値の変更

上司 ? : おい新人。世界の都市圏人工TOP5のランキングと、その中で最大の都市を表示するコードを書いてみろ。ランキングは以下の通りやで。

1位:Tokyo
2位:Jakarta
3位:Delhi
4位:Manila
5位:Seoul

新人 ? : はい!わかりました!

上司 ? : こういう感じで出力されればOKやで。この中で最大の都市は東京やからな。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Tokyo です。

新人 ? : [ Dart List sort print ? ]

新人 ? : なるほどな、これコピペして DisplayAlphabetically() の中に書いとけばええか!

void display(List<String> strings) {
  (strings..sort()).forEach(print);
}

新人 ? : あとは上司に指定されたリストを使って表示すればいけるやろ!

var biggestCitiesRanking = ['Tokyo', 'Jakarta', 'Delhi', 'Manila', 'Seoul'];
print('アルファベット順に表示: ');
DisplayAlphabetically().display(biggestCitiesRanking);
print('この中で最も大きいのは ${biggestCitiesRanking.first} です。');

biggestCitiesRankingfirst を指定しているのですから、最大の都市である Tokyo が出力されて欲しいです。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Delhi です。

新人 ? : なんでや!なんでTokyoじゃなくてDelhiなんや!

どうやら DisplayAlphabetically().display() を実行することで biggestCitiesRanking が勝手に書き換えられているようです。

例えば上のような実装がされていると、本来は 表示するだけ の関数を実装したつもりが、 勝手にリストの中身を変更 してしまい 、結果間違った値を表示したというわけです。

これは DisplayAlphabetically().display を書いた人間が悪いという話ではありません。このような値の変更を行わないことをプログラマーに徹底させることは非常にナンセンスです。

そしてこの問題への最も最適な取り組み方は、 biggestCitiesRanking変更不可能にする(Immutable) にするということです。

上司 ? : おいドアホ!ちゃんとImmutable使わんかい!

Immutable(値が変更不可能)

オブジェクト指向言語では、Immutabilityを人力で管理しようとすることでバグ、パフォーマンス上のリスクを発生させ、Immutability-Taxという名のメンテナンスコストを支払います。

? Immutableの代償①:巨大なクラス

Mutableなオブジェクトの受け渡しでは、オブジェクトが変更されたあとのことをアレコレ考える必要があります。
特にViewの実装や並列処理を行うようなコードにおいては、Mutableなオブジェクトの状態を推論することはとても難しく、莫大なメンテナンスコストと重大なバグを発生させる原因となります。
さきほどの例におけるオブジェクトの受け渡しはたった2個のモジュールの間に発生していましたが、モジュールが大量にある状態ではどうでしょうか。
モジュールの組み合わせの数は nCr で計算されるように爆発的に増加し、組み合わせの数だけコストが増えていきます。

残念ながら、ほとんどのオブジェクト指向の言語は Mutableの代償(Mutability-Tax) を考慮して設計されていません。そのためにプログラマーはImmutableに関するルールやアンチパターンを勉強し、ライブラリを入れて構築しなければなりません。
それらは人為的な対策なので、いつかは落とし穴にハマってしまいます。

落とし穴をもう少し深掘ってみます。


上司 ? : おい新人、顧客管理システムを想定したカスタマーデータのクラスを作ってみろ。

新人 ? : わかりました!

上司 ? : 名前をString、年齢をintで定義するところまではええな。

新人 ? : はい!

上司 ? : 次に positional parameters (位置指定パラメーター)named parameters (名前指定パラメーター) のどちらを採用するか、という議論では、のちにパラメーターが増えたときの可読性をあげるために後者を選択するで。

新人 ? : なるほど!それは賢いですね!

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
}

上司 ? : 次に新しいカスタマー情報を更新する場合を考えるで。年齢を+1するコードを書いてみろ。

var customer = Customer(name: 'John Smith', age: 34);
var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

新人 ? : こんな感じですかね?

上司 ? : そうや。例えばマーケチームから、顧客が何回サイトを訪れたかを記録したいと言われたときを考えてみろ。

新人 ? : わかりました!じゃあ visits パラメーターを追加すればいいですね!仮に12回訪問している場合こんな感じですかね…

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

上司 ? : じゃあさっき書いた、年齢を+1するコードを書いてみろ。

新人 ? : コピペでぽいっと

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

上司 ? : おいこら、そんなことしたら visits がリセットされるやないか!

新人 ? : うわ〜

上記のコードでは visits の値がリセットされてしまいます。
ここでの対応としては、 positional parameters を採用するか、 @required アノテーションで全てのパラメーターを必須とすることが考えられます。
しかし前述の通り位置指定パラメーターは可読性を落としますし、@required を全てに付与するということは、例えばカスタマーが訪れたときに visits に1を加算する処理においても全てのパラメーターを指定する必要があります。

上司 ? : これをメソッドで書くとこんな感じになるやろ?

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
  Customer copyWith({String name, String age}) =>
      Customer(name: name ?? this.name, age: age ?? this.age);
}

新人 ? : 完璧っすね!

var updatedCustomer = customer.copyWithage customer.visit + 1;

上司 ? : 甘いねん。Dartは null に弱いねん。このメソッドやと visitnull が渡されたことを認識できひんねん。

// nullは認識されないため、このコードは動きません
var customerWithoutVisits = customer.copyWith(visits: null);

上司 ? : Nullを認識させるためにはな、それぞれのフィールドに with メソッドを使ったら一応イケるねん。

class Customer{
  final String name;
  final int age;
  final int visits;
  Customer({this.name, this.age, this.visits});
  Customer withName(String name)
      => Customer(name: name, age: age, visits: visits);
  Customer withAge(int age)
      => Customer(name: name, age: age, visits: visits);
  Customer withVisits(int visits)
      => Customer(name: name, age: age, visits: visits);
}

新人 ? : フィールドがリセットされることも無いし、Nullにも対応したし、今度こそ完璧じゃないですか!

上司 ? : お前な、実際のサービス考えてみい。Customer が持つフィールド何個あると思てんねん。余裕で10個以上あるわ。

新人 ? : 確かに…。その分だけ with を書いて、その分だけ引数を渡すことになりますね。

上司 ? : そんなデカいボイラープレートで運用してたらお前絶対ミスるやろ。何よりクソ重なるわ。

新人 ? : はい…

? Immutableの代償②:ネストされた型への対応

上司 ? : コレクションと、ネストされた型が使用される場合を考えてみろ。

Mutable(変更可能)なコレクションをImmutable(変更不可能)なオブジェクトに組み込む場合には、それらを安全にコピーする必要があります。
またImmutableな型を使用する利点は高速であることですが、遅くなってしまうことが考えられます。
そして、先ほどのように with メソッドをしてしまうとネストされたフィールドの扱いが面倒になってきます。

class ShoppingBasket {
  final Customer customer;
  final List<Item> items;
  final List<Offer> offers;
  ShoppingBasket(
      this.customer,
      Iterable<Item> items,
      Iterable<Offer> offers)
    // Copy defensively to ensure immutability.
    : this.items = List.unmodifiable(items),
      this.offers = List.unmodifiable(offers);
  // TODO: add "with" method per field.
}

上司 ? : 例えば上のECサイトのカート(ShoppingBasket)に商品を追加する場合、 with を使ってこう書けるよな。

var updatedBasket = basket
    .withCustomer(basket.customer.withName(updatedName))
    .withItems([...basket.items, newItem, newItem2]);

新人 ? : でもこんなのいちいち with でコピーしてたら遅くなりませんか…?

上司 ? : …その通りや。Immutableは高速であることが利点やのに、遅くしてどないすんねんっちゅう話や。

上司 ? : 追加に関しては add で書くこともできるけどな、まあこれも同じような話や。

var updatedBasket = basket
    .withCustomerName(updatedName)
    .addItem(newItem)
    .addItem(item2);

新人 ? : なんかまたボイラープレートが大きくなりそうですね。

上司 ? : せやな。その上毎回フルコピーが実行されるから遅いしな。

結局何が実現されていれば良いのか

ここまで問答を繰り返してきましたが、理想としては以下の4点が実現されると良さそうです。
? 1. Immutableなデータが提供され
? 2. 更新が簡単かつ高速で
? 3. Nullを扱うことができ
? 4. フィールドが追加されても既存のコードに影響を与えない

上司 ? : 結論としては、ボイラープレートをコード生成するライブラリを使えっちゅう話や

新人 ? : 例えばどんなのがあるんですか?

上司 ? : Dartの独自のライブラリやと Built Value やな。

Built Valueの特徴
1. Immutableな型 (コレクションは built_collection
2. EnumClass(enumsのような機能)
3. JSONのシリアル化

上司 ? : ボイラープレートの管理は本来コストがかかるしバグを生みやすいよな。でもコード生成の場合はそのコストがかかるのは最初だけやねん。

新人 ? : なるほど…Mutableの代償みたいに税金みたいに段々重くなるコストじゃなくて入場料を払えばOKって感じですね!

上司 ? : お前もわかってきたな。 built_value を使ったらボイラープレートは生成してくれるからな、人間が書くのはこれだけでええねん。

abstract class Customer implements Built<Customer, CustomerBuilder> {
  String get name;
  int get age;
  @nullable
  int get visits;
factory Customer(void Function(CustomerBuilder) updates) = 
    _$Customer;
  Customer._();
}

新人 ? : 基本的なプロパティとアノテーションだけでいいのはありがたいですね!これならエンバグしなさそうです!

上司 ? : これはJavaでいうところの AutoValue.Builder にあたるな。ほんでその開発チームがコード生成ライブラリの必要性についてわかりやすくまとめたスライド AutoValue: what, why and how? を公開してくれてるわ。

AutoValue: what, why and how? の要約
多くのオブジェクト指向言語では、標準の言語仕様に従って 人力で Immutabilityを実現しようとすると、メンテナンスコスト、バグ、パフォーマンスの問題が発生します。つまりMutableの代償を避けるために、今度はImmutableの代償を払う事態になっています。Immutableオブジェクトのボイラープレートを生成し、管理する ライブラリを利用する ことこそが、Mutabilityへの正しい向き合い方です。

結論 freezed がおすすめです

上司 ? : さっきは built_value を例に上げたけど、最近は state_notifier とセットで freezed がよう使われてんねん。


元の記事 の公開後、 built_value より更に優秀な freezed というライブラリが発表されました。

ここでは簡単に freezed によって解決される、 built_value が劣っている点を記しておきます。
1. 大量のボイラープレートが必要となります。
2. built_valueDart1 用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。

※ コード生成については freezed もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。


新人 ? : なるほど、 freezed 使ってみます!

上司 ? : もう人力でやろうとすんなよ!


P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。

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

【Flutter, Dart】ミュータブルの代償ととイミュータブルの代償、そしてfreezed

はじめに

本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。

元記事の公開は2019年7月15日です。
本文中に登場するコードは Dart で記述されています。

The Mutability Tax では、それぞれの設計によって生じるコードメンテナンスコストのことを Tax(税金) と形容しています。
本記事では 代償 と表現します。

3点要点

  1. Mutableはバグを生みやすく、遅いです
  2. Immutableも正しく扱わないとコードが肥大化してバグを生みやすく、遅くなります
  3. コード生成(freezed)でImmutableの欠点を補いましょう

Mutable(値が変更可能)

システム間でMutableなデータを渡す疎結合モジュールを持つシステムは、Mutability-Taxという名のメンテナンスコストを支払います。

? Mutableの代償①:予期せぬ値の変更

上司 ? : おい新人。世界の都市圏人工TOP5のランキングと、その中で最大の都市を表示するコードを書いてみろ。ランキングは以下の通りやで。

1位:Tokyo
2位:Jakarta
3位:Delhi
4位:Manila
5位:Seoul

新人 ? : はい!わかりました!

上司 ? : こういう感じで出力されればOKやで。この中で最大の都市は東京やからな。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Tokyo です。

新人 ? : [ Dart List sort print ? ]

新人 ? : なるほどな、これコピペして DisplayAlphabetically() の中に書いとけばええか!

void display(List<String> strings) {
  (strings..sort()).forEach(print);
}

新人 ? : あとは上司に指定されたリストを使って表示すればいけるやろ!

var biggestCitiesRanking = ['Tokyo', 'Jakarta', 'Delhi', 'Manila', 'Seoul'];
print('アルファベット順に表示: ');
DisplayAlphabetically().display(biggestCitiesRanking);
print('この中で最も大きいのは ${biggestCitiesRanking.first} です。');

biggestCitiesRankingfirst を指定しているのですから、最大の都市である Tokyo が出力されて欲しいです。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Delhi です。

新人 ? : なんでや!なんでTokyoじゃなくてDelhiなんや!

どうやら DisplayAlphabetically().display() を実行することで biggestCitiesRanking が勝手に書き換えられているようです。

例えば上のような実装がされていると、本来は 表示するだけ の関数を実装したつもりが、 勝手にリストの中身を変更 してしまい 、結果間違った値を表示したというわけです。

これは DisplayAlphabetically().display を書いた人間が悪いという話ではありません。このような値の変更を行わないことをプログラマーに徹底させることは非常にナンセンスです。

そしてこの問題への最も最適な取り組み方は、 biggestCitiesRanking変更不可能にする(Immutable) にするということです。

上司 ? : おいドアホ!ちゃんとImmutable使わんかい!

Immutable(値が変更不可能)

オブジェクト指向言語では、Immutabilityを人力で管理しようとすることでバグ、パフォーマンス上のリスクを発生させ、Immutability-Taxという名のメンテナンスコストを支払います。

? Immutableの代償①:巨大なクラス

Mutableなオブジェクトの受け渡しでは、オブジェクトが変更されたあとのことをアレコレ考える必要があります。
特にViewの実装や並列処理を行うようなコードにおいては、Mutableなオブジェクトの状態を推論することはとても難しく、莫大なメンテナンスコストと重大なバグを発生させる原因となります。
さきほどの例におけるオブジェクトの受け渡しはたった2個のモジュールの間に発生していましたが、モジュールが大量にある状態ではどうでしょうか。
モジュールの組み合わせの数は nCr で計算されるように爆発的に増加し、組み合わせの数だけコストが増えていきます。

残念ながら、ほとんどのオブジェクト指向の言語は Mutableの代償(Mutability-Tax) を考慮して設計されていません。そのためにプログラマーはImmutableに関するルールやアンチパターンを勉強し、ライブラリを入れて構築しなければなりません。
それらは人為的な対策なので、いつかは落とし穴にハマってしまいます。

落とし穴をもう少し深掘ってみます。


上司 ? : おい新人、顧客管理システムを想定したカスタマーデータのクラスを作ってみろ。

新人 ? : わかりました!

上司 ? : 名前をString、年齢をintで定義するところまではええな。

新人 ? : はい!

上司 ? : 次に positional parameters (位置指定パラメーター)named parameters (名前指定パラメーター) のどちらを採用するか、という議論では、のちにパラメーターが増えたときの可読性をあげるために後者を選択するで。

新人 ? : なるほど!それは賢いですね!

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
}

上司 ? : 次に新しいカスタマー情報を更新する場合を考えるで。年齢を+1するコードを書いてみろ。

var customer = Customer(name: 'John Smith', age: 34);
var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

新人 ? : こんな感じですかね?

上司 ? : そうや。例えばマーケチームから、顧客が何回サイトを訪れたかを記録したいと言われたときを考えてみろ。

新人 ? : わかりました!じゃあ visits パラメーターを追加すればいいですね!仮に12回訪問している場合こんな感じですかね…

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

上司 ? : じゃあさっき書いた、年齢を+1するコードを書いてみろ。

新人 ? : コピペでぽいっと

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

上司 ? : おいこら、そんなことしたら visits がリセットされるやないか!

新人 ? : うわ〜

上記のコードでは visits の値がリセットされてしまいます。
ここでの対応としては、 positional parameters を採用するか、 @required アノテーションで全てのパラメーターを必須とすることが考えられます。
しかし前述の通り位置指定パラメーターは可読性を落としますし、@required を全てに付与するということは、例えばカスタマーが訪れたときに visits に1を加算する処理においても全てのパラメーターを指定する必要があります。

上司 ? : これをメソッドで書くとこんな感じになるやろ?

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
  Customer copyWith({String name, String age}) =>
      Customer(name: name ?? this.name, age: age ?? this.age);
}

新人 ? : 完璧っすね!

var updatedCustomer = customer.copyWithage customer.visit + 1;

上司 ? : 甘いねん。Dartは null に弱いねん。このメソッドやと visitnull が渡されたことを認識できひんねん。

// nullは認識されないため、このコードは動きません
var customerWithoutVisits = customer.copyWith(visits: null);

上司 ? : Nullを認識させるためにはな、それぞれのフィールドに with メソッドを使ったら一応イケるねん。

class Customer{
  final String name;
  final int age;
  final int visits;
  Customer({this.name, this.age, this.visits});
  Customer withName(String name)
      => Customer(name: name, age: age, visits: visits);
  Customer withAge(int age)
      => Customer(name: name, age: age, visits: visits);
  Customer withVisits(int visits)
      => Customer(name: name, age: age, visits: visits);
}

新人 ? : フィールドがリセットされることも無いし、Nullにも対応したし、今度こそ完璧じゃないですか!

上司 ? : お前な、実際のサービス考えてみい。Customer が持つフィールド何個あると思てんねん。余裕で10個以上あるわ。

新人 ? : 確かに…。その分だけ with を書いて、その分だけ引数を渡すことになりますね。

上司 ? : そんなデカいボイラープレートで運用してたらお前絶対ミスるやろ。何よりクソ重なるわ。

新人 ? : はい…

? Immutableの代償②:ネストされた型への対応

上司 ? : コレクションと、ネストされた型が使用される場合を考えてみろ。

Mutable(変更可能)なコレクションをImmutable(変更不可能)なオブジェクトに組み込む場合には、それらを安全にコピーする必要があります。
またImmutableな型を使用する利点は高速であることですが、遅くなってしまうことが考えられます。
そして、先ほどのように with メソッドをしてしまうとネストされたフィールドの扱いが面倒になってきます。

class ShoppingBasket {
  final Customer customer;
  final List<Item> items;
  final List<Offer> offers;
  ShoppingBasket(
      this.customer,
      Iterable<Item> items,
      Iterable<Offer> offers)
    // Copy defensively to ensure immutability.
    : this.items = List.unmodifiable(items),
      this.offers = List.unmodifiable(offers);
  // TODO: add "with" method per field.
}

上司 ? : 例えば上のECサイトのカート(ShoppingBasket)に商品を追加する場合、 with を使ってこう書けるよな。

var updatedBasket = basket
    .withCustomer(basket.customer.withName(updatedName))
    .withItems([...basket.items, newItem, newItem2]);

新人 ? : でもこんなのいちいち with でコピーしてたら遅くなりませんか…?

上司 ? : …その通りや。Immutableは高速であることが利点やのに、遅くしてどないすんねんっちゅう話や。

上司 ? : 追加に関しては add で書くこともできるけどな、まあこれも同じような話や。

var updatedBasket = basket
    .withCustomerName(updatedName)
    .addItem(newItem)
    .addItem(item2);

新人 ? : なんかまたボイラープレートが大きくなりそうですね。

上司 ? : せやな。その上毎回フルコピーが実行されるから遅いしな。

結局何が実現されていれば良いのか

ここまで問答を繰り返してきましたが、理想としては以下の4点が実現されると良さそうです。
? 1. Immutableなデータが提供され
? 2. 更新が簡単かつ高速で
? 3. Nullを扱うことができ
? 4. フィールドが追加されても既存のコードに影響を与えない

上司 ? : 結論としては、ボイラープレートをコード生成するライブラリを使えっちゅう話や

新人 ? : 例えばどんなのがあるんですか?

上司 ? : Dartの独自のライブラリやと Built Value やな。

Built Valueの特徴
1. Immutableな型 (コレクションは built_collection
2. EnumClass(enumsのような機能)
3. JSONのシリアル化

上司 ? : ボイラープレートの管理は本来コストがかかるしバグを生みやすいよな。でもコード生成の場合はそのコストがかかるのは最初だけやねん。

新人 ? : なるほど…Mutableの代償みたいに税金みたいに段々重くなるコストじゃなくて入場料を払えばOKって感じですね!

上司 ? : お前もわかってきたな。 built_value を使ったらボイラープレートは生成してくれるからな、人間が書くのはこれだけでええねん。

abstract class Customer implements Built<Customer, CustomerBuilder> {
  String get name;
  int get age;
  @nullable
  int get visits;
factory Customer(void Function(CustomerBuilder) updates) = 
    _$Customer;
  Customer._();
}

新人 ? : 基本的なプロパティとアノテーションだけでいいのはありがたいですね!これならエンバグしなさそうです!

上司 ? : これはJavaでいうところの AutoValue.Builder にあたるな。ほんでその開発チームがコード生成ライブラリの必要性についてわかりやすくまとめたスライド AutoValue: what, why and how? を公開してくれてるわ。

AutoValue: what, why and how? の要約
多くのオブジェクト指向言語では、標準の言語仕様に従って 人力で Immutabilityを実現しようとすると、メンテナンスコスト、バグ、パフォーマンスの問題が発生します。つまりMutableの代償を避けるために、今度はImmutableの代償を払う事態になっています。Immutableオブジェクトのボイラープレートを生成し、管理する ライブラリを利用する ことこそが、Mutabilityへの正しい向き合い方です。

結論 freezed がおすすめです

上司 ? : さっきは built_value を例に上げたけど、最近は state_notifier とセットで freezed がよう使われてんねん。


元の記事 の公開後、 built_value より更に優秀な freezed というライブラリが発表されました。

ここでは簡単に freezed によって解決される、 built_value が劣っている点を記しておきます。
1. 大量のボイラープレートが必要となります。
2. built_valueDart1 用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。

※ コード生成については freezed もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。


新人 ? : なるほど、 freezed 使ってみます!

上司 ? : もう人力でやろうとすんなよ!


P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。

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

ボードゲーム「カタン」の得点を計算するAndroidアプリを作ってみた。

カタンとは

(Wikipedia)

『カタンの開拓者たち』(カタンのかいたくしゃたち、Die Siedler von Catan)、または『カタン』は、ドイツで生まれたボードゲームである。1995年にコスモス社から販売された。
大航海時代に発見された無人島を複数の入植者たちが開拓していき、もっとも繁栄したプレイヤーが勝利するという内容のボードゲーム。

DSC_1005.JPG

なぜアプリを作ろうと思ったか

複雑な得点計算ルール

カタンはいろいろなルールがある、けっこう複雑なボードゲームです。
複雑な点の1つに、その得点計算ルールがあげられます。

  • 「都市」を作ると2点
  • 「開拓地」を作ると1点
  • 連続した5本以上の「街道」を作成したプレイヤーのうち、最も長い距離の連続した街道を作成しているプレイヤーに2点(最長交易路)
  • 「騎士カード」の使用枚数が3枚以上のプレイヤーのうち、最も使用枚数が多いプレイヤーに2点(最大騎士力)
  • 「最長交易路」「最大騎士力」は、より長い街道を持つ/より多く騎士カードを使用したプレイヤーが現れた時点で、該当するプレイヤーが入れ替わる
  • 「発展カード」の一種「勝利ポイントカード」を獲得すると1点

どこかで起きているかもしれない会話

A「今、誰が点数1番高いっけ?」
B「Cが1番高いよ!」
C「いや、最長交易路忘れてるじゃん。最長交易路の2点足してDが1番だよ」
D「あれ?いつの間にか俺よりBの方が交易路長くなってね?」
A「ほんと?1,2,3,4・・・ほんとだ!じゃあDじゃなくてBに+2点で、Bは6点だね!」
B「違うよ、私は7点だよ」
A「あれ?ああ、都市なのに開拓地だと思って1点で計算しちゃってた」

Androidアプリを作ろう

  • 各プレイヤーの情報を記録し、得点を計算して表示するアプリがあれば、上記のような会話をすることがなく、プレイに集中できると考えました。
  • ちょうどスマホアプリを勉強しようと思っていた時期だったので、まず最初の練習として、このカタンの得点計算アプリの開発を試みました。
  • 自分はAndroidユーザーなので、Androidアプリを選択しました。

完成したアプリ

Google Play
GitHub

UI

  

機能

  • プレイヤーの情報を記録する
    • プレイヤー名
    • 都市、開拓地の数
    • 連続する最長の街道(交易路)の長さ
    • 騎士カードの使用枚数
    • 発展カードのポイント数
  • 得点を計算する
    • 都市:2点
    • 開拓地:1点
    • 最長交易路:5つ以上の最も長い交易路を持つプレイヤーに2点
    • 最大騎士力:3回以上の最も多く騎士カードを使用したプレイヤーに2点
    • 発展カード:1点
  • ゲームの状況を表示する
    • プレイヤーの情報
    • プレイヤーの得点
    • 最長交易路、最大騎士力

開発過程

パッケージ構成

  • こちらのサイトを参考にさせていただき、パッケージ構成を決めました。
  • 当初、パッケージ名をcom.exampleにしてしまっていたら、Google Playにリリース申請をする際にエラーになってしまったので、こちらのサイトを参考にパッケージ名を変更しました。

苦労したこと①:同じ部品を複数個入れ子にするレイアウトについて

このアプリのレイアウトは
・メイン画面に、最大4人の「プレイヤー情報」を表示する
・1プレイヤーにつき、都市・開拓地・最長交易路・騎士力・発展の5つの「数情報」を表示する
という入れ子構造になっています。

入れ子にしたい部品毎にxmlを分けて、includeタグを使い、親となるxmlに子のxmlを書き加えていきました。

メイン画面:4人分のプレイヤー情報を子レイアウトに持つ

activity_main.xml
    <include
        android:id="@+id/player1"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toTopOf="parent" />

    <include
        android:id="@+id/player2"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player1" />

    <include
        android:id="@+id/player3"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player2" />

    <include
        android:id="@+id/player4"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player3" />


プレイヤー情報:5つの数情報の領域を子レイアウトに持つ

player_info.xml
    <include
        android:id="@+id/cities"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/player_name"
        app:layout_constraintEnd_toStartOf="@+id/settlements"/>

    <include
        android:id="@+id/settlements"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/cities"
        app:layout_constraintEnd_toStartOf="@+id/roads"/>

    <include
        android:id="@+id/roads"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/settlements"
        app:layout_constraintEnd_toStartOf="@+id/knights"/>

    <include
        android:id="@+id/knights"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/roads"
        app:layout_constraintEnd_toStartOf="@+id/develops"/>

    <include
        android:id="@+id/develops"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/knights"
        app:layout_constraintEnd_toEndOf="parent"/>

数情報:タイトル、数、+ボタン、-ボタン

single_count.xml
    <TextView
        android:id="@+id/count_title"
        (省略)
        app:layout_constraintWidth_min="45dp"/>

    <TextView
        android:id="@+id/count"
        (省略)
        app:layout_constraintVertical_weight="3"/>

    <Button
        android:id="@+id/up"
        (省略)
        app:layout_constraintDimensionRatio="W,1:1"/>

    <Button
        android:id="@+id/down"
        (省略)
        app:layout_constraintDimensionRatio="W,1:1"/>

ここで困った問題が発生しました。
どのプレイヤーのどのボタンを押しても、プレイヤー1の都市の数ばかり更新されてしまいます。
同じ画面に同じ要素を複数個includeしているため、ボタンのIDが一意にならず特定できないことが原因でした。
最終的に、押されたボタンを特定するときに、ボタンのIDだけではなく、ボタンの親要素のIDも参照するように記述することで解決しました。

sample.kt
//実際の実装ではリストを使ってfor文にしたりもしていますが、イメージはこんな感じ

//例1
if (view.parent.parent = findViewById(R.id.player1)) { 
    //処理 
}

//例2
if (view = findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<Button>(R.id.up)){
    //処理
}

参考
一つのlayout xmlを複数includeした時にViewへの参照を取得する方法

よく解説サイトには、includeタグを使う際はmergeタグも同時に使うようにと記載があります。
しかし、mergeタグを使ってしまうと、階層構造ができず、一番親のxmlに全てフラットに書かれているようになる(親要素という概念が無くなる)ため、親要素のIDによって押されたボタンを区別することができなくなってしまうようです。

参考
Androidの<merge>の大きな欠点?

苦労した点②:ボタンの色を、xmlではなくkotlin側で指定

カタンには、赤・オレンジ・青・クリーム色の4色のコマがあります。それぞれのプレイヤーに別々の色のコマが渡されるので、赤のコマは誰々、オレンジのコマは誰々というように、コマの色でプレイヤーの区別が付きます。

そこで、それぞれのプレイヤーの領域の色をコマの色に対応させるUIを考えました。

要素の色などは、xml側で指定するのが一般的なようですが、
上で述べたように、同じ部品を別々のプレイヤーで使い回しているので、xml側で色を指定することはできず、kotlin側でプレイヤーに応じた異なる色を、同じ部品に対して指定しなくてはいけません。

sample.kt
//kotlin側でテキストカラーを指定する
view.setTextColor(red)
//kotlin側で背景色を指定する
view.setBackgroundColor(red)

このあたりは簡単に見つかったのですが、
ボタンのデザインをプレイヤー毎に変える方法がなかなか見つからず苦労しました

結論として
①ボタンを定義するxmlファイルをres/drawable/button.xmlとして作成する
②GradientDrawableクラスとしてそのxmlを読み込む
③読み込んだクラスに対して色などを変更する
④view.backgroundに、読み込んだボタンのクラスを指定する

という方法に辿り着きました。

MainActivity.kt
       //ボタンを定義
        val btn: GradientDrawable =
            ResourcesCompat.getDrawable(
                resources,
                R.drawable.button,
                null
            ) as GradientDrawable
        btn.setColor(pink)
        btn.setStroke(2, red)
        //+ボタン
        val upView =
            findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.up)
        upView.setTextColor(red)
        upView.background = btn
        //-ボタン
        val downView =
            findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.down)
        downView.setTextColor(red)
        downView.background = btn

苦労したこと③:いろいろな画面サイズにレイアウトを対応させる

当初、それぞれの部品の大きさをハードコーディングしてしまっていたせいで、レイアウト崩れが発生してしまいました。

Android Studioの公式を見ると、いろいろな画面サイズでレイアウトが崩れないための方法がいろいろ書かれていたのですが・・・

レイアウト サイズのハード コーディングを避ける
さまざまな画面サイズに合わせて調整できる柔軟なレイアウトにするには、ほとんどのビュー コンポーネントの幅と高さに、ハードコードされたサイズではなく "wrap_content" と "match_parent" を使用します。

これがよく分かりませんでした。wrap_contentは、コンテンツの大きさが大きければ、それに合わせて無限にビューの大きさが広がってしまい、画面からはみ出してしまいます。
「画面に占めるビューの大きさ(比率)を固定にして、コンテンツの大きさを、ビューの大きさに合わせて変化させる」ことが重要に思いますし、その点でwrap_contentはその逆・・・「コンテンツの大きさに合わせてビューの大きさを変える」という仕様になっており、画面レイアウトが崩れてしまうと感じてしまいます。(間違っていたら教えてください)

結論として、いろいろな画面サイズでレイアウトが崩れないようにするために以下の方法を用いました。
公式では紹介されていませんでしたが、比率の指定にとても助けられました。

  • 部品サイズのハードコーディングは極力避ける
    • 今回の開発では、一部(RESET,QUITボタンの大きさやマージン指定)のみハードコーディングを残しました。
  • wrap_contentの使用も極力避ける
    • 一部を除きwrap_contentをやめました
  • できるだけwidth=0dp,height=0dpとし、他の要素との制約比率を使ってどんな画面サイズでも応用できる表現で、位置と大きさを表現する
    • 他の要素との制約
      • app:layout_constraintStart_toStartOf
      • app:layout_constraintStart_toEndOf
      • app:layout_constraintEnd_toEndOf
      • app:layout_constraintEnd_toStartOf
      • app:layout_constraintTop_toTopOf
      • app:layout_constraintTop_toBottomOf
      • app:layout_constraintBottom_toTopOf
      • app:layout_constraintBottom_toBottomOf
    • 比率
      • app:layout_constraintHorizontal_weight
      • app:layout_constraintVeritical_weight
      • app:layout_constraintWidth_percent
      • app:layout_constraintVertical_bias
      • app:layout_constraintDimensionRatio
  • テキストサイズもハードコーディングを極力避け、android:autoSizeTextType="uniform"を使用する

どこまで動作確認を行うか?

調べたところ、360dp*640dpの画面サイズが多数を占めていることが分かりました。
2016年発売Android端末のdp解像度まとめ
Android端末の画面サイズと解像度
画面幅のシェアの統計情報

また、幅320-410dpに対応できていれば良いのではないかという記載が多数見つかりました。

幅は320-410dpの範囲で動作確認をすれば良いとして、高さはどの範囲が動作確認の目安なのか?と疑問に思いましたが、それについての記載は見つけられませんでした。

多くのアプリは、高さに関しては縦スクロールを使って表示できれば良いので、高さについては特に制約として考えていないのかもしれません。

ですが、今回開発したアプリは、スクロールさせることなく、全てのプレイヤーの得点を同時に画面に映して、ユーザーが確認できるようにしたかったので、縦スクロールは使えません。結局、320-410dpにほぼ対応するような、533-845dpの高さ範囲で動作確認を行いました。

まとめ

今回学んだこと

  • 基本的な画面の作り方(Constraint Layout)
  • ボタン押下による処理の実装
  • 画面遷移の実装
  • 画面回転の実装
  • 戻るボタンの実装
  • ダイアログの実装
  • 端末サイズに依存しない画面レイアウト作成
  • Google Play へのリリース

今回やらなかったこと

  • サーバーサイドの処理
  • DB(Realmとか)
  • 非同期通信
  • カメラやBlueToothや位置情報など「スマホらしい」機能の利用
  • Twitterのような、縦スクロールのあるデザイン(RecyclerViewを使うようなデザイン)

最後に

ニッチなアプリではありますが、「自分の作ったものをリリースする」という経験を初めてできた点は良かったと考えています。
とても簡単なアプリではありますが、Androidアプリの第一歩を踏み出せたと感じています。

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

Kotlin Coroutineにおける実行スレッドについて

Dispatchers

Kotlinでコルーチンを使用する際、Dispatchersによってどのスレッドで実行するかを選択することができます。

GlobalScope.launch(Dispatchers.Main) {
    // 何かしらの処理
}

Dispatchersは、コルーチンの実行スレッドを表します。

種別 説明
Defalut バックグラウンドスレッド。リストの並び替えやJSONのパースなどのCPUに負荷がかかる処理向け。
IO バックグラウンドスレッド。データベース処理やファイルの読み書きおよびネットワーク処理といった入出力処理向け。
Main メインスレッド。UI処理用。delayで遅延させる場合もここに含む。

withContext

コルーチンの中で実行スレッドを切り替えるには、withContextを使用します。

class MainActivity : AppCompatActivity(), CoroutineScope {
...
// コルーチンを使用する際のデフォルトのDispatchersを設定する
override val coroutineContext: CoroutineContext
        get() = SupervisorJob() + Dispatchers.Main
...

launch {
    // ここではメインスレッドとして処理される
    withContext(Dispatchers.IO) {
        // withContextによってバックグラウンドスレッド(入出力処理)に切り替わる
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin Coroutineにおける実行スレッドの選択について

Dispatchers

Kotlinでコルーチンを使用する際、Dispatchersによってどのスレッドで実行するかを選択することができます。

GlobalScope.launch(Dispatchers.Main) {
    // 何かしらの処理
}

Dispatchers.Mainは、コルーチンの実行スレッドを表しています。
主な種別は以下の通りです。

種別 説明
Defalut バックグラウンドスレッド。リストの並び替えやJSONのパースなどのCPUに負荷がかかる処理向け。
IO バックグラウンドスレッド。データベース処理やファイルの読み書きおよびネットワーク処理といった入出力処理向け。
Main メインスレッド。他のDispatchersでの処理終了後に結果をUIに反映したり、delay関数でnミリ秒後にUIを表示させるなどのUI処理用。

withContext

コルーチンの中で実行スレッドを切り替えるには、withContextを使用します。

class MainActivity : AppCompatActivity(), CoroutineScope {
...
// コルーチンを使用する際のデフォルトのDispatchersを設定する
override val coroutineContext: CoroutineContext
        get() = SupervisorJob() + Dispatchers.Main
...

launch {
    // ここではメインスレッドとして処理される
    withContext(Dispatchers.IO) {
        // withContextによってバックグラウンドスレッド(入出力処理)に切り替わる
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter/ Dart】flutter build apk --release で生成した.apkをインストール&起動すると直後に動作を停止してしまう現象への対応

概要

デバッグビルドflutter build apk --debugで生成した.apkは正常に動作するのに、リリースビルドflutter build apk --releaseで生成した.apkは起動直後にクラッシュしてしまう現象にしばらくの間悩まされていました。
一つ解決策を見つけたので、備忘と共有のために記します。

解決策

android/app/build.gradleに以下の設定を追加することで解消できました。

build.gradle
buildTypes {
   release {
      // ...
      shrinkResources false // 追加
      minifyEnabled false // 追加
   }
}

shrinkResourcesはリソースの圧縮で、minifyEnabledはR8によるコードの圧縮(実行の際に不要と判断したコードを削除するプロセス)のことです。それぞれを無効にすることで解決しました。

原因(と思われるもの)

R8によるコードの圧縮の際に本来は必要なコードまで削除されてしまい、起動直後にアプリが停止してしまっていたのではないかと推察しています。

副作用

コードやリソースが圧縮されなくなってしまうため、注意が必要です。

参考

developer.android.com

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

[Android] Realmのデータ引っこ抜き

デザイナーなのにエンジニアさんが逃げちゃったのでエンジニアしてるし,せっかくなのでQitta書いてみた

環境

Mac Catalina 10.15.4
Android Studio 4.0.1

前提

adbコマンド打てない方は打てるようにしてください.
良い記事いっぱいあります.

方法 adbコマンド編

かなり面倒な構造になっているけどadbコマンドで直接コピーしたらpermissionなんとかっていうエラーでコピーできない仕様になってるみたいで、一度端末のsdcardに保存してからDesktopに保存してます

$ adb shell
$ run-as <package name>
sdcard/Download配下にrealmファイルをコピーしているよ
$ cp files/default.realm /sdcard/Download
Desktopに保存しているよ
$ adb pull /sdcard/Download/default.realm /Users/<user name>/Desktop/

方法 Realm Studio編

https://realm.io/jp/products/realm-studio/

先ほどコピーしたdefault.realmを表示するとデータの中身が見れます.

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

Androidエミュレータで指紋認証を実行する方法

F1(Mac の場合は Command+/)キーを押してExtended controlsウィンドウを開きます。
Screenshot from 2020-08-17 10-57-52.png

メニュー内の「Fingerprint」から指紋認証をエミュレータ上で実行することが出来ます。

指紋データの登録

設定→セキュリティ→指紋登録画面を開きます。

TOUCH THE SENSORをクリックしてFinger1の指紋を登録します。

指紋認証を実行する

あとは指紋認証を使用したいときに、事前に登録しておいたFinger1を指定してTOUCH THE SENSORをクリックするだけで指紋認証に通るようになります。登録してないFinger2などの選択すると指紋認証を失敗させることもできます。

Peek 2020-08-17 11-20.gif

参考:Android Emulator 上でアプリを実行する

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

Android Architecture ComponentのLiveDataの使い方

はじめに

今回はAndroid Architecture Component(以下AAC)にあるLiveDataに関する説明をしていきます。
とっても便利なものなので覚えておいて損はないです。

EditTextに入力した値をTextViewにそのまま表示させるアプリを作りながら説明していきます。

今回説明しないこと

AACの説明
BindingAdapter等の説明
ViewModelのこと

環境

appレベルのbuild.gradleに以下の記述をしてください。

build.gradle
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.fragment:fragment-ktx:1.2.5"

LiveDataとは

LiveDataの概要

LiveDataとは監視可能なデータホルダークラスです。
RxJavaなどでよくある監視とは違い、Androidのライフサイクルに応じた監視が可能です。

まずはサンプルを見ていきましょう。
以下のクラスとファイルを用意してください。

  • MainActivity
  • MainFragment
  • fragment_main.xml
MainViewModel
class MainViewModel(application: Application) : AndroidViewModel(application) {

    val liveData: MutableLiveData<String> = MutableLiveData()
}

今回は簡単なサンプルなのでプロパティは1つだけです。
このMutableLiveDataというのは変更可能なLiveDataです。

これで監視可能なStringのプロパティができました。

このViewModelをActivityで生成します。

MainFragment
class MainFragment : Fragment() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return super.onCreateView(inflater, container, savedInstanceState)
    }
}

ViewModelの生成方法に関する説明はこちらにちょっと書いてあるので参考にしてください。

データバインディングとは

データバンディングの概要

データ バインディング ライブラリは、プログラムではなく宣言形式を使用して、レイアウト内の UI コンポーネントをアプリのデータソースにバインドできるサポート ライブラリです。

アプリを作っていく

fragment_main.xmlの一番上の行で右クリックをして、Show Action Context -> Convert to dataBinding layoutをクリックすると

fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

    </data>

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

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

こんな感じになります。

そしてFragment側でこのレイアウトを読み込むためにこんな感じにしていきます。

MainFragment
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = FragmentMainBinding.inflate(inflater, container, false).let {
        it.root
    }

EditTextに入力したものをそのままTextViewに表示させるためにまずはEditTextとTextViewを用意します。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

    </data>

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

        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_margin="16dp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/edit_text"
            android:layout_margin="16dp"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

そしてxmlで先ほどのliveDataを使うためにViewModelを読み込みます。

dataタグの中に

fragment_main.xml
 <variable
            name="viewModel"
            type="com.example.hogehoge.MainViewModel" />

そしてEditTextには

<EditeText
  android:text="@={viewModel.liveData}" />

TextViewには

<TextView
  android:text="@{viewModel.liveData}" />

と加えます。

しかしこれだけではxmlにViewModelは渡せていません。Fragment側からViewModelを渡してあげないといけません。なのでこうしましょう

MainFragment
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = FragmentMainBinding.inflate(inflater, container, false).let {
        it.lifecycleOwner = viewLifecycleOwner
        it.viewModel = viewModel
        it.root
    }

こうすることでEditTextに入力したものがリアルタイムでTextViewに表示されるはずです。

まとめ

Android Architecture Componentは新しいライブラリなので詳しく解説されている記事があまり多くないので、初心者向けの記事を今後も書いていきたいと思います。

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