20200722のAndroidに関する記事は8件です。

【Android】またversionCodeインクリメントし忘れた!【Play Store】

背景

ぼく「Google Play Consoleにapkファイルアップロードしたぞー!(versionCode: 3)」
ぼく「あっ!修正しわすれてる!修正して、と。」
ぼく「expoのビルドだとBuild queue消化まで長いんだよな・・・また数十分待ちだよ」
ぼく「やっとビルドできた!アップロードだ!(versionCode: 3)

GooglePlayConsole「既にversionCode:3はライブラリにアップロードされて・・・」

ぼく「ま、また数十分待たされるのー!?」

Tips

それ、アーティファクト ライブラリから削除できます!artifact-lib.png

  1. リリース管理 > アーティファクトライブラリ
  2. 対象のバージョンコードのゴミ箱ボタン > 押下 => 削除!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】Firebase を使って SNS っぽいアプリを作るために学んだこと

趣味の魚捌きの延長で、魚を捌くのが好きな人のための SNS アプリ、 "Sengyo" を作ってリリースしました。

iOS
https://apps.apple.com/us/app/id1523325680

Android
https://play.google.com/store/apps/details?id=com.tsuyoshichujo.sengyoproduct

Firebase を始めとした今まで雰囲気で使っていた技術の勉強や OS の新機能などの実験台として何かひとつ具体的なアプリがあると良いな、というモチベーションで作ったこのアプリですが、ひとまずアプリとして最低限形になったので、まずはこのアプリを作るために Firebase の使い方で学んだことを振り返ってみたいと思います。

なお、この記事では どのようなページを見てどのような情報を得ながらアプリを作ったか という内容が中心で、「こうプログラムを書けばこう動く」というような具体的なところまでは書いていません。

「同じようなアプリを作ってみたいけど、何をどう調べてどう作り始めたら良いかイメージできない」という方の役に立てればと思って書いた記事です。

学んだこと

このアプリはアプリ本体を Flutter で、サーバーサイドを Firebase で作っています。 Firebase はさらに細かく見ると、

  • Firestore
  • Cloud Storage
  • Firebase Authentication

という3つのプロダクトを利用しています。

この記事では、 Sengyo アプリでこれらの Firebase の各プロダクトを使うために何を見てどのような情報を得たのかをそれぞれまとめていきます。

なお、Sengyo アプリのコードは GitHub に上げてあります。必要に応じて参照してみてください。(ただし「良いコード」ではない点に注意です!)

chooyan/Sengyo | GitHub

Firestore

Firestore は文字列や数値、日時など様々な型のデータを保存することができるデータベースです。

Sengyo アプリでは、主にユーザーの入力した投稿データを保存するデータベースとしてこの Firestore を利用しています。

SQL 脳から脱却する

そんな Firestore を使う上でまず知っておく必要があるのが、 Firestore は MySQL や Oracle のような RDBMS とは違い、 JSON 形式でデータを管理する NoSQL データベース であるということです。

NoSQL データベースを使ってすでに何かプロダクトを作ったことがある方にとっては当たり前のことかもしれませんが、表形式の RDB と JSON 形式の NoSQL ではデータ構造の設計方法や考え方が全く異なります。自分は NoSQL でプロダクトを作る知見も経験もほぼなかったため、まずはそこから調べる必要がありました。

ということでまず見つけたのが↓の動画です。

The Firebase Database For SQL Developers | Youtube

この動画では SQL を知っている開発者向けに NoSQL を説明しています。Firestore に限らず「NoSQL とは」を SQL と比較する形で説明してくれているので、アプリの SQLite やバックエンドの MySQL などを触ったことのある自分にとってはちょうど良い内容でした。

詳しくは動画を観ていただければと思いますが、ここでは

  • RDB ではサービスの機能に依らずデータそのものに着目して正規化する一方で、 NoSQL ではサービスが一番必要とする形式のままデータを保存すること
  • 同じデータが複数箇所に保存されることを怖がる必要はないこと
  • SQL を NoSQL に置き換える例

など、 SQL 脳を脱却するための考え方を得ることができました。

動画も1つ数分で終わるライトなものなので、それほど身構えずに観られるのも良い点です。

Firestore という製品を知る

NoSQL データベースというジャンルについての理解ができたら、次は Firestore というプロダクトについて知る必要があります。

これも Firebase が公式で動画を出しているので、順番に観ていきます。

Get to Know Cloud Firestore | Youtube

Firestore は NoSQL の考え方をベースに、様々な機能や設計を取り入れています。例えば

  • 「ドキュメント / コレクション」ベースのデータ管理
  • 配列型とコレクションの使い分け
  • リアルタイム アップデート
  • オフライン時のデータアクセス
  • セキュリティルール

などです。
上記の動画はこれらの機能をざっと把握するのにとても役に立ちました。

若干スピーカーの英語が早口なのと、所々でちょっとしたジョークが入って置いていかれることがあったものの、この内容は Firebase のドキュメントに文章としてもまとまっているため、分からない部分はそちらで補完していけばだいたい大丈夫でした。

Cloud Firestore | Firebase

Flutter で Firestore を扱う

NoSQL の考え方と Firestore という製品がある程度できたら、あとは Flutter アプリから使ってみるだけです。

Flutter で Firestore を扱うための cloud_firestore プラグインが公開されているため、これを使います。

cloud_firestore | pub.dev

単純なデータの出し入れの方法はパッケージのサンプルコードを見ればだいたい使い方は分かるようになっています。まずは自分の Google アカウントで Firebase プロジェクトを作って README の通りに動かしてみると良いでしょう。Firebase はちょっと触ってみる分には無料で使えるようになっています。

「参照型のフィールドを setData() したい場合には DocumentReference 型のインスタンスを value として渡す」など、細々としたところで README には書かれていない部分もありましたが、実際に動かしながら確認すればそれほど難しいことはなかった記憶です。

なお、Sengyo アプリでは Firestore のデータを出し入れするコードは以下のあたりです。

https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/article_repository.dart

何かの参考になれば。

Cloud Storage

Cloud Storage は静的なファイルの保存場所として使える製品です。Sengyo では画像ファイルの保存に利用しています。

Cloud Storage も Flutter 向けにパッケージが公開されていますので、 Firestore と同じようにサンプルコードを見ながら動きを確認すればそれほど大きな問題はないと思います。

firebase_storage | pub.dev

Cloud Storage はフォルダを作ってファイルを保存するシンプルなもので、基本的には Firebase のドキュメントとパッケージの README を読めばある程度使えるようになっています。

Sengyo アプリでは以下のあたりのソースコードが参考になるかと思います。

https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/image_file_repository.dart

Firebase Authentication (メールリンク認証)

Firebase Authentication はユーザーの認証をするための仕組みを提供してくれる製品です。これを使うことでログイン機能が簡単に、安全に実現できます。

一言に「認証」と言っても、ドキュメントを見ると分かる通りその方法は様々で、シンプルな ID/パスワード で認証する方法から電話番号、SNS認証、また匿名認証というものも用意されています。

そのため、まずはそれぞれの認証方法に必要なユーザー情報やメリット・デメリットを理解した上で、サービスの要件(想定ユーザーの使いやすさ、セキュリティ要件など)に応じて利用する方法を取捨選択していく必要があります。

Sengyo アプリでは、管理コストの問題からなるべく少ない情報のやりとりでログインを実現したいことや、まずは分かりやすく1通りだけユーザーにログイン方法を示したかったため、メールリンク認証 を採用しています。

この記事でもメールリンク認証について書いていきます。

メールリンク認証とは

メールリンク認証は、ユーザーがアプリから入力した メールアドレスに対してログイン用リンクを送信 し、メーラーで リンクをタップすればアプリが開いてログインが完了する 、という仕組みです。

ユーザーが入力されたメールアドレスの持ち主であることを根拠に本人を認証する方法で、ユーザーとしてはパスワードを入力する必要もなく手軽に安心してアカウントを作成できる、というわけです。

このあたりは、 Firebase のドキュメントを読むことである程度イメージができるかと思います。

iOS でメールリンクを使用して Firebase 認証を行う | Firebase
Android でメールリンクを使用して Firebase 認証を行う | Firebase

一方で実装面では若干手順が多くなるため、ひとつひとつ何が起きているのかを整理して理解する必要があります。

メールリンク認証の流れ

まずは、ユーザーがメールアドレスを入力したあと、裏でどのようなことが起こっているのかを理解します。

同じことは Firebase のドキュメントでも説明されていますが、実際の処理の流れに加えて Firebase コンソール上の設定方法なども織り交ぜて説明されているため、ここでは事前準備などは省いて実際のサービスで何が起きているか、に着目して流れをまとめてみます。

  1. ユーザーのメールアドレスにログイン用のリンクが含まれたメールを送信する
  2. ユーザーがメール内のリンクをタップして開く。
  3. アプリが開かれたリンクを拾ってアプリを起動する
  4. リンクに含まれるリクエストパラメータとメールアドレスをセットにして Firebase へ送信する
  5. Firebase がリクエストパラメータとメールアドレスのセットが正しいことを検証する
  6. 検証の結果がアプリに通知される

ここで、3の「開かれたリンクを拾ってアプリを起動する」を実現するために、もう1つ別の Firebase 製品である Firebase Dynamic Links を利用します。そのため、まずは Firebase Dynamic Links について学ぶ必要があります。

Firebase Dynamic Links とは

アプリには、ユーザーが特定の URL を開こうとした時にブラウザではなくアプリを起動する仕組みがあります。これを、 Android では ディープリンク、iOS では Universal Link と呼んでいます。

この仕組みをアプリに取り入れるためには、アプリ側だけでなくサーバー側にも「その URL の保有者とアプリの開発者が確かに同一であること」を証明するための設定が必要であったり、アプリがインストールされていなかった場合の挙動を定める必要があったり、またそれらの設定を Android / iOS それぞれのプラットフォームの仕様に従って行わなければならないなど、様々な労力が発生します。

Firebase Dynamic Links は、そのあたりの面倒な設定を Firebase コンソール上で一括で行ってくれるサービスです。

Firebase コンソール上の "Dynamic Links" メニューから上記のような設定項目(iOS / Android それぞれについての、リンクで起動するアプリやインストールされていない場合の挙動など)、トラッキングの有無などを設定して Dynamic Link を作成するだけなのでとても便利です。

ただしこれをメールリンク認証で使う場合の設定方法についてはメールリンク認証のドキュメント内に概要だけ書かれていたものの、具体的にどの項目をどう設定すれば良いかの記述は見つからなかったため、今回は割と雰囲気と試行錯誤で設定しました。

このあたりは追って別の記事で詳しくまとめてみたいと思います。

アプリ の実装

メール認証と Dynamic Links の設定が Firebase コンソール上でできたら、次にアプリの方もいろいろ実装する必要があります。

このあたりも先ほどの Firebase のドキュメントを見ながら、 iOS は XCode から Associated Domain の設定などを追加、 Android は AndroidManifest.xml ファイルに Intent-Filter を追加して、指定したドメインのリンクが開かれたときに、ブラウザではなく自分のアプリが開かれるようにしていきます。

次に、「メールリンク認証の流れ」で書いた通り、アプリのプログラムとして

  • メールを送信する処理
  • Dynamic Link からアプリが開かれたことを判定する処理
  • Firebase Authentication へリンクとメールアドレスを送信する処理

を実装します。

これも firebase_auth という Firebase Authentication 全般を Flutter で扱うプラグインが公開されているため、これを使います。

firebase_auth | pub.dev

ただし、メールリンク認証についてはサンプルコードや README では触れられていないため、 API ドキュメントやコードを読んで適切なメソッドを見つける必要があります。

FirebaseAuth class | firebase_auth

結論から言うと、アドレスを指定してログイン用のメールを送るためには sendSignInWithEmailLink() を、 Dynamic Link でアプリが開かれたあと FirebaseAuth にリンクとメールアドレスを送って認証するためには signInWithEmailAndLink() メソッドを利用します。

さらに、 Dynamic Link からアプリが開かれた際の処理については、 firebase_dynamic_links というプラグインが別に用意されているため、こちらを使います。

firebase_dynamic_links | pub.dev

基本的な使い方や考え方は README に書いてある通りです。

注意点としては、 Dynamic Link からアプリが開かれたとき、すでに裏でアプリが実行中で初期化処理などが実行されないパターンも考慮する必要がある、ということです。この点についてはサンプルコードでも FirebaseDynamicLinks.instance.onLink で Dynamic Link から起動された時に呼び出されるコールバックを初回起動時に登録しておくコードが書かれていますので、参考にすると良いでしょう。

また、このサンプルコードはメールリンク認証で使うことを考慮したものではないため、うまく signInWithEmailAndLink() メソッドとつなげてあげることや、ログイン後の処理に自分のアプリの要件に応じてつなげてあげる必要があります。

このあたりはサンプルコードも少なく試行錯誤した上で実装しましたが、一応 Sengyo アプリとしては以下の LoginBloc クラスにまとめたような実装になっていますので、何かの参考になれれば嬉しいです。

https://github.com/chooyan-eng/Sengyo/blob/master/lib/bloc/login_bloc.dart

まとめ

以上、 Sengyo アプリを作るために利用した Firestore, Cloud Storage, メールリンク認証 をそれぞれ使うために参照したものや学んだことをまとめてみました。

細かなソースコードレベルの説明や具体的な設定の手順などは最初に書いた通り省いていますので、そのうちそれぞれについては別の記事として詳しく書いてみたいと思います。特にメールリンク認証についてはそのまま参考にできる記事などが全然見つからなかったため、優先的に書きたいところです。

Firebase と Flutter は、ちょっとした個人アプリをささっと作ろうと思ったときにとても役に立つ組み合わせだと思います。

初めて触る人にとっては馴染みのないものもあるかもしれませんが(もしかしたら最近は Firebase から入るような人も多いかもしれませんが)、この記事でまとめたように参考にできる記事や動画はたくさん公開されていますので、それらをひとつずつ見ながら実際に触ってみることでとりあえずひとつのアプリとして形にできる環境が整っています。

「それっぽい SNS アプリをとりあえず作ってみる」ために、何をどれくらい知る必要があるのか、この記事を読んでイメージが伝えられれば嬉しいです。

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

Amazon Kinesis Video Streams WebRTC をiOSとAndroid動かしてみた

はじめに

Amazon Kinesis Video Streams に ビデオチャットサービスが加わりました。
ブラウザ(JavaScript)向けのSDKだけでなく、組み込み用途のC言語SDKや、iOS/Androidといったモバイルアプリ向けのSDKも用意されており、ビデオチャットサービスを実装することができます。
今回は iOS, Android のサンプルを動かし、iOS <-> Android アプリ間でビデオ通話をしてみようと思います。

Getting Started
iOS SDK
Android SDK

AWSでリソースを作成する

AndroidのSDKのREADMEの手順が分かりやすかったので、それに倣って進めていきます。

Amazon Kinesis Video Streams Android WebRTC SDK README.md

シグナリングチャンネルを作成する

Kinesis Video Streams でシグナリングチャンネルを作ります。

  • [シグナリングチャンネルを作成]を選択します。今回は東京リージョンに作りました。
    channel1.png

  • シグナリングチャンネル名を入力します。今回は[demo-channnel]としました。
    channel2.png

CognitoUserPool を作成する

CognitoUserPool を作成します。

  • [ユーザープールの管理]を選択し、次のページで[ユーザープールの作成]を選択します。
    user1.png

  • プール名を入力します。今回は[MyUserPool]としました。プール名を入力したら[デフォルトを確認する]を選択し、次のページでデフォルト値を変えずに[プールを作成]を選択します。
    user3.png

  • オレンジの矢印の先にある[プールId]をメモし、左のナビゲーションから [アプリクライアント]を選択します。
    user5.png

  • 新しいアプリクライアントを作成します。まずアプリクライアント名を入力します。ここでは[MyAppClient]という名前で作成しました。

  • アプリクライアントの作成が完了したら[アプリクライアント ID]と[アプリクライアントのシークレット]をメモします。[詳細を表示]を選択すると表示されます。
    user6.png

CognitoIdentityPool を作成する

CognitoIdentityPool を作成します。

  • [IDプールの管理]を選択し、[新しいIDプールの作成]を選択します。
    user1.png

  • プール名を入力します。今回は[MyIdentityPool]としました。
    identity1.png

  • [認証プロバイダー]の[Cognito]のタブを開き、[ユーザープールId]と[アプリクライアントId]に先ほどメモをした値を入力し、作成します。
    identity2.png

  • Cognito_MyIdentityPoolAuth_RoleCognito_MyIdentityPoolUnauth_Role の2つロールが作成されます、一つはログイン済みユーザーに付与されるロールで、もう一つはログインされていないユーザーに付与されるロールです。つまりログイン済みのユーザーは kinesisvideo にアクセスできるように以下のポリシーをアタッチします。

{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Action":[
            "cognito-identity:*",
            "kinesisvideo:*"
         ],
         "Resource":[
            "*"
         ]
      }
   ]
}

identity3.png

Android

AWSのリソースの作成ができたので、Android のサンプルアプリをビルドしていきます

  • Androidのサンプルプロジェクトをダウンロードします。
$ git clone https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-android.git
  • AndroidStudio を起動し、 Open an existing Android Studio project で先ほどダウンロードしたサンプルプロジェクトを開きます。

  • /res/raw/awsconfiguration.json にメモした IdentityPoolId, UserPoolId, UserPoolAppClientSecret, UserPoolAppClientSecret, UserPoolAppClientId を貼り付けます。IdentityPoolIdはリージョンも含めて入力する必要があるので気をつけてください。

  • 実機にインストールし、ユーザーを作成します。入力したメールアドレスに二要素認証のコードが送られてくるので間違えずに入力します。

  • ユーザーの作成とログインができたら [Start Viewer]でビデオチャットを始めます。

  • PCで Kinesis の demo-channel を開き、Master でビデオを流すと PC と Android アプリでビデオチャットができます。

カビゴンがPCのカメラで、エヴァ初号機がAndroidのカメラで撮ってます。
ブラウザ <-> Androidアプリ間でビデオ通信ができました。

pc-to-android.jpg

iOS

基本的な設定はAndroidで既に終わっているので、設定を追加してビルドします。

$ git clone git@github.com:awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios.git
$ cd amazon-kinesis-video-streams-webrtc-sdk-ios
$ cd Swift
$ pod install  # もし cocoapods が入っていなかったらインストールが必要

awsconfiguration.jsonを追加。

awsconfiguration.json
{
  "Version": "1.0",
  "CredentialsProvider": {
    "CognitoIdentity": {
      "Default": {
        "PoolId": "YOUR_IDENTITY_POOL_ID",
        "Region": "ap-northeast-1"
      }
    }
  },
  "IdentityManager": {
    "Default": {}
  },
  "CognitoUserPool": {
    "Default": {
      "AppClientSecret": "YOUR_APP_CLIENT_SECRET",
      "AppClientId": "YOUR_APP_CLIENT_ID",
      "PoolId": "YOUR_USER_POOL_ID",
      "Region": "ap-northeast-1"
    }
  }
}

Constants.swift を開いて入力

let CognitoIdentityUserPoolRegion: AWSRegionType = AWSRegionType.APNortheast1
let CognitoIdentityUserPoolId = "YOUR_USER_POOL_ID"
let CognitoIdentityUserPoolAppClientId = "YOUR_APP_CLIENT_ID"
let CognitoIdentityUserPoolAppClientSecret = "YOUR_APP_CLIENT_SECRET"
let cognitoIdentityPoolId = "YOUR_IDENTITY_POOL_ID"

ログインし、ビデオ通話を開始します。
先ほど作成したユーザーを使いまわしても大丈夫です。

ios-to-android.png

TODO: Twilio との料金の比較

参考にさせて頂いた記事

Amazon Kinesis Video Streams WebRTC を動かしてみたた

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

【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの三つで実現してみる①

Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。

本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。

一、providerでテーマ切替&国際化

provider: ^4.3.1  (providerパッケージをpub.devから取得)

1.テーマ切替

ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
color1.pngcolor2.pngcolor3.pngcolor4.pngcolor5.pngcolor6.png

1.1.状態クラス

_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。

changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。

theme_state.dart
import 'package:flutter/material.dart';

class ProviderThemeState extends ChangeNotifier{
  ThemeData _themeData;//テーマ
  int _colorIndex;//色ボタン選択中の状態を記録

  ProviderThemeState(this._themeData, this._colorIndex);

  void changeThemeData(ThemeData themeData, int colorIndex){

    this._themeData = themeData;
    this._colorIndex = colorIndex;
    notifyListeners();

  }

  int get colorIndex => this._colorIndex;
  ThemeData get themeData => this._themeData;
}

1.2.管理する必要がある部分をラップする(wrap)

MultiProviderは複数のproviderを入れることができる。widgetをwrapperだけでなく他のコンポーネントでもラップやカバーできる。

main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';

void main() {
  runApp(Wrapper(child: MyApp()));
}

class Wrapper extends StatelessWidget{

  final Widget child;
  Wrapper({this.child});

  @override
  Widget build(BuildContext context) {

    final initThemeData = ThemeData(
      primaryColor: Colors.green,
    );

    final initIndex = 1;

    return MultiProvider(providers: [
      ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),
    ],child: child,);

  }

}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderThemeState>(builder: (context,state,widget) =>MaterialApp(
      title: "状態管理Demo",
      theme: state.themeData,
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ));
  }
}

1.3.状態の使用、メソッドの実行

Provider.of(context)よりConsumerのほうは結構細かい対象を絞れるため、widget再ビルドの消費コストが削減される。また、ConsumerはwidgetのBuildContextが不要で使えるのも一大メリット。

ボタンの押下により、changeThemeData()が呼び出されConsumerの状態の状態が更新される。

home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/theme_state.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    var screenWidth = MediaQuery.of(context).size.width;
    var statusBarH = MediaQuery.of(context).padding.top;
    var naviBarH = kToolbarHeight;

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter状態管理",style: TextStyle(color: Colors.white),),
      ),
      drawer: Drawer(
        child: Consumer<ProviderThemeState>(builder: (context, state, widget) {
          return Column(
            children: <Widget>[
              Container(
                width: screenWidth,
                height: statusBarH + naviBarH,
                color: state.themeData.primaryColor,
              ),
              SizedBox(height: 10.0),
                  Row(
                    children: <Widget>[
                      Padding(padding: EdgeInsets.all(10)),
                      Wrap(children: <Widget>[
                        RaisedButton(
                          color: Colors.green,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.green), 1);

                          },
                          child: Text("緑",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                        RaisedButton(
                          color: Colors.red,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
                          },
                          child: Text("赤",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                        RaisedButton(
                          color: Colors.blue,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.blue), 3);
                          },
                          child: Text("青",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                      ],),

                    ],
                  ),
              Divider(),
              SizedBox(height: 10,),
              Row(children: <Widget>[
                Padding(padding: EdgeInsets.all(10)),
                SizedBox(width: 15,),
                RaisedButton(
                  color: state.themeData.primaryColor,
                  onPressed: () {

                  },
                  child: Text("日本語",style: TextStyle(color: Colors.white),),
                ),
                SizedBox(width: 15.0,),
                RaisedButton(
                  color: state.themeData.primaryColor,
                  onPressed: () {

                  },
                  child: Text("English",style: TextStyle(color: Colors.white),),
                ),
              ],),

                ],
          );
        }),
      ),
      body: ListView(
        children: <Widget>[
          Consumer<ProviderThemeState>(builder: (context, state, widget) {
            return Center(
              child: Column(
                children: <Widget>[
                  SizedBox(height: 10.0),
                  Container(
                    width: 200,
                    height: 300,
                    color: state.themeData.primaryColor,
                  ),
                  SizedBox(height: 10.0),
                  Text(
                    "学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し",
                    style: TextStyle(
                        color: state.themeData.primaryColor,
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 30.0),
                  FloatingActionButton(onPressed: (){

                  },
                  backgroundColor: state.themeData.primaryColor,
                  child: Icon(Icons.check)),
                ],
              ),
            );
          })
        ],
      ),
    );
  }

}

2.言語切替

flutter_localizations:
sdk: flutter (多言語をpubspec.yamlで配置)

ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。

language1.pnglanguage2.pnglanguage3.pnglanguage4.png

2.1.言語データを準備する

language_data.dart
class LanguageData {

  static final EN = {
    "title":"Flutter State Management",
    "greenBtn":"Green",
    "redBtn":"Red",
    "blueBtn":"Blue",
    "analects":"To learn without thinking is blindness, \nto think without learning is idleness (Confucius)."
  };

  static final JP = {
    "title":"Flutter状態管理",
    "greenBtn":"緑",
    "redBtn":"赤",
    "blueBtn":"青",
    "analects":"学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し"
  };

}

2.2.言語データを使用するためのクラス

i18n.dart
import 'package:flutter/material.dart';
import 'language_data.dart';

class I18N {

  final Locale locale;
  I18N(this.locale);
  static Map<String, Map<String, String>> _localizedValues = {
    "en": LanguageData.EN,
    "ja": LanguageData.JP,
  };

  static I18N of(BuildContext context) {
    return Localizations.of(context, I18N);
  }

  get title {
    return _localizedValues[locale.languageCode]['title'];
  }

  get greenBtn {
    return _localizedValues[locale.languageCode]['greenBtn'];
  }

  get redBtn {
    return _localizedValues[locale.languageCode]['redBtn'];
  }

  get blueBtn {
    return _localizedValues[locale.languageCode]['blueBtn'];
  }

  get analects {
    return _localizedValues[locale.languageCode]['analects'];
  }

}

2.3.多言語のデリゲートクラス

I18nDelegate.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'i18n.dart';

class I18nDelegate extends LocalizationsDelegate<I18N> {
  @override
  bool isSupported(Locale locale) {
    ///サポートする言語
    return ['en', 'ja'].contains(locale.languageCode);
  }

  ///現在言語環境下の文字列をロード
  @override
  Future<I18N> load(Locale locale) {
    return SynchronousFuture<I18N>(I18N(locale));
  }

  @override
  bool shouldReload(LocalizationsDelegate<I18N> old) {
    return false;
  }

  //グローバル静的デリゲート
  static I18nDelegate delegate = I18nDelegate();

}

2.4.状態クラス

localeというフィールドしか使わない

locale_state.dart
import 'package:flutter/material.dart';

class LocaleState extends ChangeNotifier{
  Locale _locale;//地域
  LocaleState(this._locale);

  factory LocaleState.jp()=>
      LocaleState(Locale('ja', 'JP'));

  factory LocaleState.en()=>
      LocaleState(Locale('en', 'US'));

  void changeLocaleState(LocaleState state){
    _locale=state.locale;
    notifyListeners();
  }

  Locale get locale => _locale; //言語ゲット
}

2.5.状態の使用、メソッドの実行

Consumer() の他に Consumer2() から Consumer6() まであり、得たい状態値の種類数によって使い分けることができる。

2.5.1.多言語化の配置
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';
import 'provider/locale_state.dart';
import 'provider/I18nDelegate.dart';

void main() {
  runApp(Wrapper(child: MyApp()));
}

class Wrapper extends StatelessWidget{

  final Widget child;
  Wrapper({this.child});

  @override
  Widget build(BuildContext context) {

    final initThemeData = ThemeData(
      primaryColor: Colors.green,
    );

    final initIndex = 1;

    return MultiProvider(providers: [
      ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),//themeのprovider
      ChangeNotifierProvider(create: (_) => LocaleState.jp()),//localeのprovider
    ],child: child,);

  }

}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>MaterialApp(
      title: "状態管理Demo",
      localizationsDelegates: [GlobalMaterialLocalizations.delegate,
      GlobalWidgetsLocalizations.delegate,
        I18nDelegate.delegate//言語デリゲート
      ],
      locale: localState.locale,
      supportedLocales: [localState.locale],
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ));
  }
}

2.5.2.多言語化の実行
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/locale_state.dart';
import 'provider/theme_state.dart';
import 'provider/i18n.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    var screenWidth = MediaQuery.of(context).size.width;
    var statusBarH = MediaQuery.of(context).padding.top;
    var naviBarH = kToolbarHeight;

    return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>Scaffold(
        appBar: AppBar(
          backgroundColor: themeState.themeData.primaryColor,
          title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),),
        ),
        drawer: Drawer(
          child: Consumer2<ProviderThemeState,LocaleState>(builder: (_,themeState,localState,__) {
            return Column(
              children: <Widget>[
                Container(
                  width: screenWidth,
                  height: statusBarH + naviBarH,
                  color: themeState.themeData.primaryColor,
                ),
                SizedBox(height: 10.0),
                Row(
                  children: <Widget>[
                    Padding(padding: EdgeInsets.all(10)),
                    Wrap(children: <Widget>[
                      RaisedButton(
                        color: Colors.green,
                        onPressed: () {
                          themeState.changeThemeData(ThemeData(primaryColor: Colors.green), 1);

                        },
                        child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.red,
                        onPressed: () {
                          themeState.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
                        },
                        child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.blue,
                        onPressed: () {
                          themeState.changeThemeData(ThemeData(primaryColor: Colors.blue), 3);
                        },
                        child: Text(I18N.of(context).blueBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                    ],),

                  ],
                ),
                Divider(),
                SizedBox(height: 10,),
                Row(children: <Widget>[
                  Padding(padding: EdgeInsets.all(10)),
                  SizedBox(width: 15,),
                  RaisedButton(
                    color: themeState.themeData.primaryColor,
                    onPressed: () {
                      localState.changeLocaleState(LocaleState.jp());
                    },
                    child: Text("日本語",style: TextStyle(color: Colors.white),),
                  ),
                  SizedBox(width: 15.0,),
                  RaisedButton(
                    color: themeState.themeData.primaryColor,
                    onPressed: () {
                      localState.changeLocaleState(LocaleState.en());
                    },
                    child: Text("English",style: TextStyle(color: Colors.white),),
                  ),
                ],),

              ],
            );
          }),
        ),
        body: ListView(
          children: <Widget>[
            Consumer2<ProviderThemeState,LocaleState>(builder: (_, themeState, localeState,__) {
              return Center(
                child: Column(
                  children: <Widget>[
                    SizedBox(height: 10.0),
                    Container(
                      width: 200,
                      height: 300,
                      color: themeState.themeData.primaryColor,
                    ),
                    SizedBox(height: 10.0),
                    Text(I18N.of(context).analects,
                      style: TextStyle(
                          color: themeState.themeData.primaryColor,
                          fontSize: 18.0,
                          fontWeight: FontWeight.bold),
                    ),
                    SizedBox(height: 30.0),
                    FloatingActionButton(onPressed: (){

                    },
                        backgroundColor: themeState.themeData.primaryColor,
                        child: Icon(Icons.check)),
                  ],
                ),
              );
            })
          ],
        ),
      ),

    );
  }

}


まとめ

本文ではProviderでテーマや多言語の切替という多状態を管理してみました。Flutterの状態管理はandroid, iOSと違い、多分多くの方が悩み続けていると思いますね。Flutterの状態管理について、代表的にprovider,BLoC,reduxがありますので、次回ではBLoCパターンをご紹介していきたいと思います。

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

【Android 9.0 Pie Java】RecyclerViewの余白部分にsetOnTouchListenerを実装する

初めに

リスト表示に便利なRecyclerViewですが、デフォルトのままだと余白部分をタッチした際にイベントを発火させることができません。
RecyclerViewの余白部分をタッチしてソフトキーボードを閉じる為に実装を模索したので共有したいと思います。

32aeddc0c59703301a8fc30cf57374b6.gif

実装方法

まずRecyclerViewに

android:touchscreenBlocksFocus="true"

を追記します。

あとはonCreateViewやonCreatedViewでリスナーを実装するだけです。

Fragment.java
recyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // キーボードを隠す
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(),
                InputMethodManager.HIDE_NOT_ALWAYS);
        // 背景にフォーカスを移す
        RecyclerView recyclerView = view.findViewById(R.id.recyclerView);
        recyclerView.requestFocus();
    }
});

以上です。

どなたかの参考になれたら幸いです。

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

【Android 9.0 Pie Java】RecyclerViewの余白部分にsetOnTouchListenerを実装してソフトキーボードを閉じる

初めに

リスト表示に便利なRecyclerViewですが、デフォルトのままだと余白部分をタッチした際にイベントを発火させることができません。
RecyclerViewの余白部分をタッチしてソフトキーボードを閉じる為に実装を模索したので共有したいと思います。

32aeddc0c59703301a8fc30cf57374b6.gif

実装方法

まずRecyclerViewに

android:touchscreenBlocksFocus="true"

を追記します。

fragment.xml
<LinearLayout
    android:id="@+id/scrollView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:isScrollContainer="false"
    app:layout_constraintBottom_toTopOf="@+id/footerBorder"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/headerBorder">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:touchscreenBlocksFocus="true"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

あとはonCreateViewやonCreatedViewでリスナーを実装するだけです。

Fragment.java
RecyclerView recyclerView = view.findViewById(R.id.recyclerView);
recyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // キーボードを隠す
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
        // 背景にフォーカスを移す
        recyclerView.requestFocus();
        return false;
    }
});

以上です。

どなたかの参考になれたら幸いです。

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

ViewModel考察

本記事を書く経緯

ViewModelをよくこんな感じに使っていましたが

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()

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

これはある程度の「魔術」だと思って、viewModelsの中で何をやっているのか、なんでActivityのライフサイクルを超えて生存できるのかを気にしていませんでしたが、Activity間で同じViewModelを共有したい要件に出会ったので、本格的にViewModel周りを調査してみました。

ViewModelの生成

まずAndroid KTXを使わないでViewModelを利用する原始的な書き方を見てみます。

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // (Deprecated) viewModel = ViewModelProviders.of(this).get(MainViewModel::class)
        viewModel = ViewModelProvider(this).get(MainViewModel::class)
    }
}

Android KTXが出る前まではたぶん一番こういう書き方がよく見られます。
ただ、この書き方はデフォルトで用意されているViewModelProvider.Factoryを使用する前提になっているし、登場人物はまだ出揃っていないので、解説には不向きです。もうちょっとカスタマイズした書き方を見てみます。

// 2. Activityは元からViewModelStoreOwnerを実装しています
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // 1. ViewModelProvider(owner: ViewModelStoreOwner, factory: ViewModelProvider.Factory)
        val provider = ViewModelProvider(this, MainViewModelFactory())
        viewModel = provider.get(MainViewModel::class)
    }
}

// 3. 実際にViewModelを生成するFactoryクラス
class MainViewModelFactory : ViewModelProvider.Factory() {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        // ViewModelを手動生成する
        return MainViewModel() as T
    }
}

class MainViewModel : ViewModel() {
    ...
}

上記のViewModelProviderViewModelStoreOwnerViewModelProvider.FactoryViewModelの取得に関わる主要人物です。一個ずつ見ていきます。

ViewModelStoreOwner

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

ViewModelStoreOwnerViewModelStoreを返す役割を持っています。ViewModel保存に関わっています。
ViewModelStoreは自体一種のHashMapで、ViewModelを保存/返す役割を持っています。

ViewModelStoreOwnerが返すViewModelStoreは毎回違うのか、それとも同じViewModelStoreを返すかはViewModelStoreOwnerの実装次第です。
ActivityはこのViewModelStoreOwnerを実装しています。回転などで再生成されたActivityでも一律前と同じViewModelStoreのインスタンスを返すように実装されているので、Activityのライフサイクルを超えてViewModelは生存できました。

ViewModelProvider.Factory

public interface Factory {
    /**
     * Creates a new instance of the given {@code Class}.
     * <p>
     *
     * @param modelClass a {@code Class} whose instance is requested
     * @param <T>        The type parameter for the ViewModel.
     * @return a newly created ViewModel
     */
    @NonNull
    <T extends ViewModel> T create(@NonNull Class<T> modelClass);
}

ViewModelProvider.FactoryViewModel生成に関わっています。
Factoryを使わなくても例えば普通にMainViewModel()を呼んだら生成できますが、Factoryを通すことで 生成する/しない生成のタイミングViewModelProviderに移譲します。

ViewModelProvider

public class ViewModelProvider {
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        // mViewModelStoreに取得しようとしているViewModelがあればそのまま返す
        // なければmFactoryを使ってViewModelを生成した上で、mViewModelStoreに入れてあとに返す
    }
}

ViewModelProviderは大体デザインパターンでいうとBuilderパターンのDirectorの役割です。FactoryViewModelStoreOwner制御してViewModelを返します。

Factoryを使ってViewModelを生成し、ViewModelStoreに入れる作業をするし、ViewModelがすでにViewModelStoreに入っていたら生成しないで、直接ViewModelを返します。

ActivityのViewModelStoreOwnerの実装

回転などで再生成されたActivityでも一律前と同じViewModelStoreのインスタンスを返すことで、Activityのライフサイクルを超えてViewModelは生存できました。

ではActivityはどうやって同じViewModelStoreを返すのか、これはComponentActivity1内のgetViewModelStoreの実装を見ればわかります。
結論を先にいうと、getLastNonConfigurationInstanceonRetainNonConfigurationInstanceを使って実現してました。

class ComponentActivity extends androidx.core.app.ComponentActivity implements ViewModelStoreOwner, ... {
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }
}

mViewModelStoreがまだ初期化されていなければ、getLastNonConfigurationInstanceからViewModelStoreを取得します。

このgetLastNonConfigurationInstanceはあまり馴染みがないので、調べてみました。どうもViewModelより以前からある仕組みです。

public Object getLastNonConfigurationInstance()
public Object onRetainNonConfigurationInstance()

Activityがdestroyされる前にonRetainNonConfigurationInstanceは呼ばれます。この関数をオーバーライドして任意のオブジェクトを返せば、次にActivityが再生成されたときにgetLastNonConfigurationInstanceから完全同一オブジェクトを取得できます(そのオブジェクトは死なないです)。

ComponentActivityはこの仕組を利用して、Activityがdestroyされる前にViewModelStoreonRetainNonConfigurationInstanceで一旦退避させて、Activityが再生成されてgetViewModelStoreが呼ばれたときに同じオブジェクトを返すようにしました。

このgetLastNonConfigurationInstanceonRetainNonConfigurationInstanceは、今では使われない機能らしいですが、昔はそれなりに活躍していたらしいです。だが今はComponentActivity内ではonRetainNonConfigurationInstancefinalにしているので、オーバーライドさせてもらえないです。
(代わりに名前が似ているド同等機能のonRetainCustomNonConfigurationInstancegetLastCustomNonConfigurationInstanceがありますが、この2つはfinalになってないのでオーバーライドできますが、Deprecatedな関数なので、あえて使う必要がないでしょう。)

結論

ViewModel生成Factoryの責務で、ViewModelの保存・生存期間ViewModelStoreOwner(ViewModelStore)の責務です。

Activity間でViewModelを共有したければ、共通のViewModelStoreを持てばいいです。
そのViewModelStoreはせめてActivityより長生きしないとだめなので、Activityより長いライフサイクルを持つところにそのViewModelStoreを置くしかないです。

つまり手取り早くApplicationに置くことになりそうです。(もしくはDaggerでViewModelStoreのシングルトンを注入します)

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

大学生が1週間でFlutterアプリを学んでリリースした過程(5日目)

こんにちはシオンです。

昨日デザインがやっと決まり、コードを書き始めたところまで行きました。
ここまで来ればあとはコードを勉強しながら決めたデザインのものを形にしていくだけです。
残す課題は時間だけ、期限までに終わらせるというのが今回のチャレンジで一番大事になってくるのでここまできたら何がなんでも間に合わせます。

プログラミングの進捗としては昨日FlutterでHelloWorldと表示することに成功しました。このプログラミングを学ぶ人が絶対に最初に通るLv1の状態から今日あすの2日でコードを書き切って、ラスト1日でリリースしてみせます。

少し話は変わりますが、qiitaではこういう投稿はあまり良くないようですね。
qiitaについて深く理解せずブログのような投稿をしてしまいました。この一連の投稿が終わったら改めたいと思います。

ではやっていきます。

■まず画面をデザイン通りに構成する

コードを書き切ると言っても、本当にFlutterに対して無知なので闇雲に書いて行っても終わりが見えてきそうにないです。

ちなみにどのくらい無知なのかというと
現在私が持っているFlutterの知識は、
Flutterとは「フルッター」ではなく「フラッター」と読み、表記はアルファベットで構成されている。また、今日の私の朝ごはんは食パンにバターを塗ったトーストであるが、私が持つFlutterの知識と私の今日の朝食については全く関係がない。

書くことがなさすぎて今日の朝食を紹介するしかないくらい無知ということです。よろしくお願いいたします。

ということで、一旦画面遷移などの処理は後回しにしてデザインの画面を形だけ再現してそのあとにカウントアップだったりの処理を書いていきたいとおもいます。

ちなみにデザインはこれです。(昨日のです。)

1.png

この画面を作っていく上で必要になってくるのは構成部品とそのレイアウトの知識です。
今回必要になってくる部品としては
・テキスト
・テキスト入力フォーム
・ボタン
・画像
この4つの部品があれば大丈夫そうです。

では次にFlutterでのレイアウトの仕方を学んでいきます。
昨日Flutterでプログラミングしていく上で重要になってくるのがウィジェットというものだというのを学びました。
おそらくこのウィジェットの中に今回使う、画像だとかボタンだとかを持ったウィジェットがいるはずなので、まずはレイアウトは考えず必要な部品を
表示してみます。

また3画面目から先はシンプルな構造ですぐに構成できると思うので、これは処理を書きながらにしていきます。

では1画面目と2画面目をやっていきます。

■1画面目と2画面目の外観を実装

2.png

1画面目はこんな感じに。なかなか時間がかかりました。
初めての言語でコードを書くときは勝手がわからない+関数やクラスの名前がわからない、で苦戦するのですが、その分うまく行ったときの「できた!!」っていう快感がたまらないですね。

新しくゲームを始めてレベル1からスタートしている感じがしてすごく楽しいです。早くこの一週間チャレンジというチュートリアルをクリアして冒険に出かけます。

続いて2画面目。
3.png

2画面目は内容入力のフォームの高さ調節が思うようにいかずこんな感じになってしまいました。
調べながらやっているのですが、まだまだFlutter自体が新しい分日本語記事が少ない印象ですかね。それでもわかりやすコードや使い方を記事にしてくださっている方が何人もいて助かりました。(本当にありがとうございます。)

まだ少ししか触ってませんが、Flutterめちゃくちゃいいです。
この2画面を作っただけでもそのコードの書きやすさにびっくりしています。
まだ記事が少ないので初めてプログラミングを学ぶ人にとっては多少難しいかもしれませんが、何かしらやったことがある人にはお勧めしたいです。

■まとめ

あとはこの画面に処理を記述していけばなんとか形にはなりそうです。
処理も一通り見たのですが、これくらいの画面遷移とカウントアップだけならすぐにできそうなので明日一気に仕上げてしまいます。

なんとなく形になりそうな気配がしてきました。

でも残すは明日と明後日のみ。。。
果たして結果は完成するのかしないのか。

残り2日間やっていきます。

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