Androidはワンツーパンチ 三歩進んで二歩下がる

プログラミングやどうでもいい話

Androidアプリ「ポケット糖質量」をリリースいたしました

こんにちは。さくらです。
Androidアプリ「ポケット糖質量」をリリースいたしました。
play.google.com

このアプリが何を出来るかといいますと、1100種類以上の食品データから糖質量などの栄養素をリスト表示・検索することが出来ます。
f:id:sakura_bird1:20170816222524p:plain

以前このブログでRailsで「ポケット糖質量サイト」を作ったという記事を書きましたが、
そのAndroidアプリ版です。
sakura-bird1.hatenablog.com


最初はサイトがレスポンシブ対応になっているので、WebViewアプリみたいなものを作ろうかなと思っていたのですが、スマホブラウザで見るとどうしても表示が間延びして使いづらいなという印象でした。
ちなみにサイトは「Material Design for BootStrap」というテンプレートを使用していて、
レスポンシブのレイアウトも自動で表示してくれます。
それはそれで大変ありがたいのですが、専用のネイティブアプリの使い勝手には敵わない
ということと、レスポンシブ部分のコーディングスキルがないので作ることにしたのでした。

シンプルなアプリなので使い方はすぐにわかると思います。
検索したり、絞り込んだり、お気に入り登録したり、コピーしたり出来ます!

アプリを作り始めたのは2017年5月ぐらいなのですが、途中モチベが下がってしまったりしてダラダラしてたら8月になってしまいました。
モチベが下がってしまったのは、昨年末ぐらいから全体的に元気が無く年齢的な不調も加わって人生全般灰色に感じていたと言いますか・・
すぐ陳腐化してしまうITスキルについても、もう追いついていけないし
やったこともみんな無駄になってしまうんじゃないかと虚しくなったりしたのでした。
(というカミングアウトw)

というわけで、ゴミアプリかもしれませんが、細々作りました。
今回は自分の学習として最近Androidでもよく見かけるモダン開発をフルで取り入れようと思って

DI
RxJava
DataBinding

を全体的に取り入れました。
すでに新しい技術でもなくなっているので、今までも部分的にこれらを取り入れたり
簡単なサンプルを作ったりしたことはあるのですが、
最初からこうした方法を全体に使ったのは初めてです。
設計概念に関わる部分のやり方を変えるときには自分のアプリだといいですよね。
気軽に試行錯誤できるし失敗したっていい!

上記の手法を取り入れるにあたって、
プロジェクト内のフォルダの構成やどんな部分でどのクラスを使うのか、
DroidKaigiの公式アプリをものすごく参考にさせていただきました。
っていうかフォルダ名とか同じにしてるし、恥ずかしいぐらい真似してます。

おかげでなんとか書けたですよ。
DroidKaigiさんありがとう、そして貴重な情報をネットに書いてくれてるみなさんありがとう!

検証はテスト端末今1台しか無いので、ちょっと心配ですがうまく動くかな〜
アプリ内で使っている素材は大体Sketchで単純なものを作りました。
Sketchでアプリ素材書き出すのはほんとに楽でよかったです。

今後はまあ気楽にやっていきます。
RailsももうちょっとやりたいしAndroidアプリも作りたいです。
iOSまで手が回らないかな。
IT系じゃなく全く他のこともやってみたいな。

シニアプログラミングネットワーク #1勉強会に行ってきました

f:id:sakura_bird1:20170429143515j:plain

こんにちは。さくらです。
先日こんな勉強会に行ってきました。

eventdots.jp

81歳のiPhoneアプリプログラマー若宮正子さんがアプリ「hinadan」を開発した件は朝日新聞、CNNをはじめとして世界中で報道されました!
だがしかし、81歳プログラマーはさらにいた!そして、さらに他にも「若い者には負けない」どころか「圧倒する」方達が!
今回はシニアプログラミングネットワーク第1回イベントとしてシニアなプログラマーの皆さんの活動を紹介していき、プログラミングを学ぶこと、そしてお年寄りに使いやすいということはどういうことなのかを考えて行きます。
最古参にして最先端のコンピューターおばあちゃん/おじいちゃんの活躍をぜひご覧ください!
81歳にしてアプリを開発して話題になった若宮正子さんの「hinadanのストーリー」を。同じく81歳にして既にアプリを3本もリリースしている方が既にいた!その鈴木富司さんのセッション。また、ハードウェアを絡めた話では遠く福井から谷川一男さんからなんとイノシシ90頭を仕留めた自作檻の話をしていただきます。そして、若宮さんが以前にアプリのレビューを行なった浪江町でのCode for Japanフェローシップの話もあって盛りだくさんです!

このすごさ・・・!
と、言ってもシニアでプログラミングをされている方は私の身近にもいらっしゃいますのでそこまでは驚かなかったですが。
(その方は70歳ぐらいですが、Androidアプリを300本ぐらいリリースされています。
東京アプリ開発者もくもく会にいつも来てくれる方で、すごーく元気をもらっています。)


客席も結構シニアでしたね。47歳の私なぞ小娘でしたね。
今日来てた人達が気軽に集まれる勉強会とかまたやってくれたら行きたいです。

写真も撮ったのですが、後ろの方に座っていたので全体的に下のほうがうまく写っていなくてすみません。
メディアの方がたくさん来ていたようなので、別途詳しい記事が上がると思います。
と思ってたら記事がありました!
フリック!ニュース /Flick!News : 何歳になってもプログラミングにチャンレンジできる! #シニアプログラミング


f:id:sakura_bird1:20170429140518j:plain

登壇者の方は実際にアプリを開発されたおばあさまおじいさまだったのですが、どの方もよどみなく喋ってプレゼン力があり、お若かったです。
わたしはあんなに喋れないですよ。
元々聡明でいらっしゃる感じでした。
みんな英語も得意なんだって(✽ ゚д゚ ✽)

若宮正子さん

82歳になったばかりだそうです。リリースしたiOSアプリ「hinadan」について。Swiftで書かれたそうです。
反響がすごく色々なメディアに取り上げられてテレビにも出演したそうです。
韓国でNAVERのカンファレンスで講演をされたそうですよー。すごいですね。
若宮さんのお話はユーモアたっぷりですごくウケていました。
主催の小泉さんとSkype(でしたっけ?すみません物忘れが・・)かなにかでやりとりしてマンツーマンで教えてもらいながら作ったそうです。


f:id:sakura_bird1:20170429141219j:plain

f:id:sakura_bird1:20170429141353j:plain

アプリのイメージ。年寄りが勝てるゲームを作りたい!という気持ちから始まったビッグプロジェクト!
f:id:sakura_bird1:20170429141620j:plain

f:id:sakura_bird1:20170429142054j:plain

f:id:sakura_bird1:20170429143118j:plain

小泉さんの開発裏話も楽しく参考になりました。
お年寄り向けのUIの話や教える時のコツとか。出来ることと出来ないことを明確に区別して、捨てるべきところを捨ててゴールに向かっていったそうです。
今後は作りたいものから逆算して教育する仕組みを作りたいそうです。
小泉さんは小学生の頃からプログラミングをされていたそうなので、すごいと思いました。

f:id:sakura_bird1:20170429143239j:plain

f:id:sakura_bird1:20170429143306j:plain

また若宮さんはコードフォージャパンの陣内一樹さんとともに浪江町のアプリ開発にも関わったそうです。
陣内さんのプレゼンも素晴らしく、利用者を置き去りにしたアプリ開発を反省させられました。
写真撮れなくてごめんなさい。

f:id:sakura_bird1:20170429160655j:plain


鈴木富司さん

勉強会当日が誕生日だそうで82歳になられたそうです🎉
既に3本もiOSアプリをリリースされてるそうです。
鈴木さんはプログラミングのスクールに通って勉強されたそうですが、一度挫折して2度目の挑戦だったそうです。
現在はiOSもくもく会によく顔を出されているそうで、若者との交流を楽しみにしているそうです。
鈴木さんのアプリ開発は趣味という言葉より使命感という言葉の方があっている感じです。
高齢者向けスマホの勉強アプリを開発されてます。
アップルのリジェクトとも戦ったそうです。すごい。

f:id:sakura_bird1:20170429144139j:plain

f:id:sakura_bird1:20170429144320j:plain

f:id:sakura_bird1:20170429144548j:plain

f:id:sakura_bird1:20170429145102j:plain

f:id:sakura_bird1:20170429145351j:plain

f:id:sakura_bird1:20170429150748j:plain

f:id:sakura_bird1:20170429150548j:plain

谷川一夫さん

Ichigo Jamって初めて知ったんですが、BASICでプログラミングできる子供用パソコンだそうです。
ichigojam.net
色々揃えても数千円で済む感じ。
このモジュールと赤外線センサーを組み合わせてイノシシの捕獲檻を自作して年間90頭のイノシシを捕まえたというすごいお話でした。
渋谷のお洒落なIT勉強会でイノシシの捕まえ方を聞くというシュールさとか色々あり過ぎるんですが、
谷川さんの問題解決力のすごさに脱帽しました。
お住いの福井ではイノシシに農作物を荒らされる被害に悩まされているそうです。
子供プログラミング教室(当日その教室の素敵な先生もいらしてましたよ)でIchigo Jamをイノシシ捕獲に使うことを思いついたそうです。
元々設計士だそうなので、CADを使った設計などもそうですが素養がおありになるなーって思いました。

f:id:sakura_bird1:20170429152824j:plain

f:id:sakura_bird1:20170429152922j:plain

f:id:sakura_bird1:20170429153159j:plain

f:id:sakura_bird1:20170429153416j:plain

f:id:sakura_bird1:20170429153604j:plain

f:id:sakura_bird1:20170429153920j:plain

f:id:sakura_bird1:20170429154401j:plain

f:id:sakura_bird1:20170429154909j:plain

f:id:sakura_bird1:20170429155311j:plain

f:id:sakura_bird1:20170429155335j:plain

f:id:sakura_bird1:20170429155453j:plain

f:id:sakura_bird1:20170429160235j:plain


こんな感じで驚きつつ楽しい時間を過ごしました。
登壇者様、開催者様ともに本当にありがとうございました。
素晴らしい活動を拝見してやる気をたくさんいただけました。

最後に、私も地頭悪い系で趣味プログラミングをやっている者ですが、自分の実感からネガティブ面もちょっと書いておこうと思います。
年取ってもやる気と好奇心があればプログラミング出来るんだー!わーわー!と盛り上がってもやっぱり難しい局面が多いと思うのです。
対象は全くのプログラミング初心者の高齢者の方です。

学習用プログラミング言語やハローワールド以上のことをやろうとすると難易度が高くなる

安定した環境で学習用に作られたプログラミング言語で学習したり、
他の言語でも画面にハローワールドを表示する(のに毛が生えた程度)といったことをやってるうちは平和に進むと思いますが、
「自分の作りたいアプリ」はそれでは実現出来なかったりするのでなんだかんだで難しくなってきます。

年をとると新しいことを覚えるのが辛くなる問題

全くの初心者が始めるのはやっぱりめちゃめちゃ大変だと思います。覚えることが膨大です。
やっと覚えても変化が早いので古い知識になっていて世間ははるか先をいってたりします。

お金がかかるかもしれない

プログラミング技術の本は一冊3000円ぐらいします。また、本だけだと問題が起きた時に一人で解決するのが難しいことがあります。
不定期で集まる勉強会のようなところに参加するのは非常によいことですが、
現役エンジニアは多忙なので、聞いたことには答えてくれてもガッツリ教えてもらうことは期待できません。
何がわからないのか言語化出来れば無料のQAサイトを頼ることが出来ますが、
その段階に至っていなければスクールなどに通ったり有料学習サービスを利用するという選択肢になるかもしれません。

英語と切っても切れない

この勉強会でも話題に出ましたが、プログラミングをすると英語のエラーメッセージを解読する必要があります。
英文を無視せず翻訳サイトで翻訳するといった手間は必要です。
プラットフォームによってはアプリ申請やリジェクトなどのやり取りを英語で行う必要があります。

その他、互換性のないアップデートやモジュール間のバージョン依存など

OSや開発ツールやライブラリのバージョンを上げると動かなくなったり、
新しいバージョンのモジュールを使いたければ今まで作ったプログラミングを修正しなければならなかったり
ということが日常茶飯事のように感じられるかもしれません。


なーんて、いろいろとやる気を削ぐようなことを書いてごめんなさい。
でも今回の勉強会の登壇者様達のようにやる人はやるので、面白そうだとかこんなのが作りたいだとかあったらとりあえず飛び込んでみてもいいですよね。
無料でリッチな開発環境が手に入ったり、世界中の凄腕エンジニアのコードを読めたり、良いことも山ほどある世界ですし!
小泉さんがおっしゃるような作りたいゴールから逆算して教育する仕組みが出来たら、もっと多くの高齢の方が気軽にプログラミングを楽しめる時代が来るかもしれません。
そうなったら楽しいですね♡









伸び縮みするCardViewを作成する(cachapa/ExpandableLayout + RecyclerView + CardViewのサンプル)

RecyclerViewを使ってリスト表示しているレイアウトがあり、その中でタップするとViewが伸縮するCardViewを表示するレイアウトのサンプルを作ったのでメモです。
ExpandableListViewと似たような表示方法です。


f:id:sakura_bird1:20170425233635g:plain:w300

こちらに全体のソースがあります。短いコードですが、ブログを読んでいてサンプルのプロジェクト全体を見たいなあって思うことがあるのでGithubにアップしてます。
github.com


開いたり閉じたりするビューを実装するのにこちらのライブラリを使用させていただいています。
github.com


まずはライブラリの導入から
app/build.gradle

dependencies {
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:cardview-v7:25.3.1'
    compile 'com.android.support:design:25.3.1'

    compile 'net.cachapa.expandablelayout:expandablelayout:2.8'
}


レイアウトです。
Activityのレイアウトファイルです。
res/layout/activity_main.xml

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="16dp" />

    </LinearLayout>
</layout>


RecyclerViewに表示する1行分のレイアウトです。
CardViewの中にExpandableLayoutを入れ子にしています。
Data Bindingを使用しています。
res/layout/recycler_item.xml

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

    <data>

        <variable
            name="viewModel"
            type="com.sakurafish.expandablerecyclerview.sample.RecyclerItemViewModel" />
    </data>

    <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginTop="8dp"
        app:cardUseCompatPadding="true"
        card_view:cardCornerRadius="4dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/expand_button"
                style="@style/TextAppearance.AppCompat.Medium"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:elevation="6dp"
                android:onClick="@{viewModel::onClickExpandButton}"
                android:padding="16dp"
                android:text="@{viewModel.expandButtonText}" />

            <net.cachapa.expandablelayout.ExpandableLayout
                android:id="@+id/expandable_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#eee"
                app:el_duration="300"
                app:el_expanded="false"
                app:el_parallax="0.5">

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <TextView
                        android:id="@+id/text1"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentTop="true"
                        android:gravity="center"
                        android:padding="10dp"
                        android:text="@{viewModel.text1}" />

                    <TextView
                        android:id="@+id/text2"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_below="@+id/text1"
                        android:gravity="center"
                        android:padding="10dp"
                        android:text="@{viewModel.text2}" />

                </RelativeLayout>

            </net.cachapa.expandablelayout.ExpandableLayout>

        </LinearLayout>
    </android.support.v7.widget.CardView>
</layout>

Activityです。
アダプターの中のonBindViewHolderでViewの伸び縮みの制御をしています。
expand()で伸び、collapse()で縮みます。

MainActivity.java

package com.sakurafish.expandablerecyclerview.sample;

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ObservableArrayList;
import android.databinding.ObservableList;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.sakurafish.expandablerecyclerview.sample.databinding.ActivityMainBinding;
import com.sakurafish.expandablerecyclerview.sample.databinding.RecyclerItemBinding;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
        ObservableList<RecyclerItemViewModel> list = new ObservableArrayList<>();
        for (int i = 0; i < 50; i++) {
            RecyclerItemViewModel viewModel = new RecyclerItemViewModel("text1 : " + i, "text2 : " + i);
            list.add(viewModel);
        }
        binding.recyclerView.setAdapter(new ListAdapter(this, list));
    }

    private static class ListAdapter extends RecyclerView.Adapter<ListAdapter.ViewHolder> {

        public ListAdapter(@NonNull Context context, @NonNull ObservableList<RecyclerItemViewModel> list) {
            this.context = context;
            this.list = list;
        }

        private final Context context;
        private final List<RecyclerItemViewModel> list;

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new ViewHolder(context, parent);
        }

        @Override
        public void onBindViewHolder(final ViewHolder holder, int position) {
            final RecyclerItemViewModel viewModel = getItem(position);
            if (viewModel.isExpanded()) {
                holder.binding.expandButton.setSelected(false);
                holder.binding.expandableLayout.expand(true);
            } else {
                holder.binding.expandButton.setSelected(false);
                holder.binding.expandableLayout.collapse(true);
            }

            viewModel.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (viewModel.isExpanded()) {
                        holder.binding.expandButton.setSelected(false);
                        holder.binding.expandableLayout.collapse(true);
                    } else {
                        holder.binding.expandButton.setSelected(true);
                        holder.binding.expandableLayout.expand(true);
                    }
                    viewModel.setExpanded(!viewModel.isExpanded());
                }
            });

            viewModel.setExpandButtonText(position + ". Tap to expand");

            holder.binding.setViewModel(viewModel);
            holder.binding.executePendingBindings();
        }

        @Override
        public int getItemCount() {
            return list.size();
        }

        public RecyclerItemViewModel getItem(int position) {
            return list.get(position);
        }

        public class ViewHolder extends RecyclerView.ViewHolder {

            RecyclerItemBinding binding;

            public ViewHolder(Context context, ViewGroup parent) {
                super(LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false));
                binding = DataBindingUtil.bind(itemView);
            }
        }
    }
}

DataBindingで使用しているビューモデルのクラスです。
RecyclerItemViewModel.java

package com.sakurafish.expandablerecyclerview.sample;


import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.NonNull;
import android.view.View;

public class RecyclerItemViewModel extends BaseObservable {

    private String expandButtonText;
    private String text1;
    private String text2;

    private View.OnClickListener onClickListener;
    private boolean expanded = false;

    RecyclerItemViewModel(@NonNull String text1, @NonNull String text2) {
        this.text1 = text1;
        this.text2 = text2;
    }

    public String getText1() {
        return text1;
    }

    public String getText2() {
        return text2;
    }

    @Bindable
    public String getExpandButtonText() {
        return expandButtonText;
    }

    public void setExpandButtonText(@NonNull String expandButtonText) {
        this.expandButtonText = expandButtonText;
    }

    public void onClickExpandButton(View view) {
        if (onClickListener != null) {
            onClickListener.onClick(view);
        }
    }

    public void setOnClickListener(@NonNull View.OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public void setExpanded(boolean expanded) {
        this.expanded = expanded;
    }

    public boolean isExpanded() {
        return this.expanded;
    }
}


以上です。

追記 2017/05/06

上記のサンプルに伸縮を表すアイコンを追加しました。
f:id:sakura_bird1:20170506212819p:plain:w200

f:id:sakura_bird1:20170506212839p:plain:w200

アイコンはGoogle製マテリアルデザインのアイコンで、ベクター画像を使用しています。
手前味噌で恐縮ですが、こちらのページの方法でxmlを追加しました。
sakura-bird1.hatenablog.com


ビューを開いた状態と閉じた状態で表示する画像を変更するのにselectorを使用しています。

res/drawable/expand_arrow.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_expand_more_black_24dp" android:state_selected="true" />
    <item android:drawable="@drawable/ic_expand_less_black_24dp" android:state_selected="false" />
</selector>

ImageViewでsrcで画像を指定する箇所を
app:srcCompat="@drawable/expand_arrow"
という風にselectorを定義したxmlファイルの名前にしておきます。

ビューを押すタイミングでsetSelected(boolean)メソッドでselectedの状態を切り替えます。
これでビューを押すたびに画像が変更されますが、せっかくなのでアニメーションも付けました。

        @Override
        public void onBindViewHolder(final ViewHolder holder, int position) {
            final RecyclerItemViewModel viewModel = getItem(position);
            if (viewModel.isExpanded()) {
                holder.binding.expandButton.setSelected(false);
                holder.binding.expandableLayout.expand(true);
            } else {
                holder.binding.expandButton.setSelected(true);
                holder.binding.expandableLayout.collapse(true);
            }

            viewModel.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (viewModel.isExpanded()) {
                        holder.binding.expandButton.setSelected(false);
                        holder.binding.expandableLayout.collapse(true);
                    } else {
                        holder.binding.expandButton.setSelected(true);
                        holder.binding.expandableLayout.expand(true);
                    }
                    // 画像を180度回転させる
                    ObjectAnimator anim = ObjectAnimator.ofFloat(holder.binding.expandArrow, "rotation", 0, 180);
                    anim.setDuration(150);
                    anim.start();

                    viewModel.setExpanded(!viewModel.isExpanded());
                }
            });
(中略)
        }