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

Flutter Form系Widgetの使い方 〜 すべてのWidgetを学習

はじめに

自身の勉強を兼ねて、Widget Catalogで公開されている全てのWidgetについての解説やソースコードを交えた使い方などをまとめています。

基本方針として、掲載しているサンプルコードは公式サイトのサンプルを流用しています。しかし、公式サイトにはサンプルコードがないものが多くあるため、その場合には動作確認済みの自身のコードを掲載しています。間違い等ありましたらご指摘頂けると助かります。

カテゴリ Widgetの種類 Qiitaまとめ記事
Accesibility アクセシビリティに関するWidget Coming Soon
Animation and Motion アニメーションに関するWidget Coming Soon
Assets, Images, and Icons Asset/Icon/画像表示に関するWidget Coming Soon
Async Asyncパターン (FutureBuilder, StreamBuilder) Coming Soon
Basics 最低限抑えるべき基本的なWidget Coming Soon
Cupertino (iOS-style widgets) iOS風の各種Widget Coming Soon
Input フォームやキー入力等の入力に関するWidget 今回
Interaction Models タッチイベントやその他操作の応答に関するWidget Coming Soon
Layout レイアウトに関するWidget Coming Soon
Material Components Android マテリアルデザインの各種Widget Coming Soon
Painting and effects ペイント, エフェクト効果等のWidget Coming Soon
Scrolling GridView等のスクロール機能を保有するようなWidget Coming Soon
Styling テーマ等のスタイリングに関わるWidget Coming Soon
Text テキスト表示に関わるWidget Flutter Text Widgetの使い方 〜 すべてのWidgetを学習

Form系 Widgets

テキスト入力用途のフォームやキー入力検出に関する以下のWidgetについて解説します。

Widget名 用途
Form FormField用のコンテナWidgetで複数のFormFieldをまとめて管理する時に利用します。
TextFormField テキスト入力のFormField Widgetです。
FormField Form内で利用するベースとなるWidgetです。これを直接利用することはなく、TextFormFieldのように、FormFieldを継承して独自のFormFieldを作成する時に利用します。
RawKeyboardListener キー入力検出用のリスナーです。

Form

Formクラス仕様

後述の複数のFormFiled Widgetをグループ化して管理するためのコンテナWidgetです。グループ化することで、ユーザが入力データの送信ボタンを押した時に、フォーム内の各入力データの形式チェックするValidatorを一括して呼び出すことが出来ます。

Formクラス
const Form({
  Key key,
  @required this.child,
  this.autovalidate = false,
  this.onWillPop,
  this.onChanged,
}) : assert(child != null),
     super(key: key);

Formは子クラス (後述のTextFormFiledなど) の最上位のコンテナとして定義します。

なお、Validatorを利用する場合には、第一引数のKeyGlobalKey<FormState>を指定する必要があります。このKeyについては別の記事にて解説予定のため、ここでは以下のサンプルコードのように必要なものだレベルで覚えてるに留めておいてください。

TextFormField

TextFormFieldクラス仕様

別途解説予定のTextField Widgetを後述のFormFieldに適用したWidgetです。つまり、FormにおけるText入力のWidgetです。

サンプルコード

サンプルとして、2つのテキスト入力を持ったフォームの例を示します。Submitボタンを押した時に各テキスト入力をチェックし、空白以外ならOKというロジックになっています。
ezgif.com-video-to-gif-2.gif

サンプルコード
class FormSampleState extends State<WidgetSample> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          TextFormField(
            decoration: const InputDecoration(
              labelText: "Email address form", // ラベル
              hintText: 'Enter your email', // 入力ヒント
            ),
            autovalidate: false, // 入力変化しても自動でチェックしない。trueにすると初期状態および入力が変化する毎に自動でvalidatorがコールされる
            validator: (value) { // _formKey.currentState.validate()でコールされる
              if (value.isEmpty) {
                return 'Please enter some text'; // エラー表示のメッセージを返す
              }
              return null; // 問題ない場合はnullを返す
            },
            onSaved: (value) => () { // this._formKey.currentState.save()でコールされる
              print('$value');
            },
          ),
          TextFormField(
            decoration: const InputDecoration(
              icon: Icon(Icons.email),
              border: OutlineInputBorder(), // 外枠付きデザイン
              filled: true, // fillColorで指定した色で塗り潰し
              fillColor: Colors.greenAccent,
              labelText: "Email address form",
              hintText: 'Enter your email',
            ),
            autovalidate: false,
            validator: (value) {
              if (value.isEmpty) {
                return 'Please enter some text';
              }
              return null;
            },
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 16.0),
            child: RaisedButton(
              onPressed: () {
                // 各Fieldのvalidatorを呼び出す
                if (_formKey.currentState.validate()) {
                  // 入力データが正常な場合の処理
                  this._formKey.currentState.save();
                }
              },
              child: Text('Submit'),
            ),
          ),
        ],
      ),
    );
  }
}

FormField

FormFieldクラス仕様

FormFieldForm内で利用するベースとなるWidgetです。後述のTextFormFieldもこれを継承しており、その他Form内で利用する独自定義Widgetを作成するなどに利用します。

サンプル

サンプルとして、独自定義Widgetを利用したフォーム入力の例を示します。初期値0のカウンタを+/-ボタンで増減するフォームを作成しています。Submitボタンを押した時に-1以外であればResult表示を更新し、それ以外はFieldにエラーメッセージが表示されます。
ezgif.com-video-to-gif.gif

サンプルコード
class FormFieldSampleState extends State<WidgetSample> {
  final _formKey = GlobalKey<FormState>();
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Form(
        key: _formKey,
        child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              CounterFormField( // 独自定義のFormField
                autovalidate: false,
                validator: (value) {
                  if (value < 0) return 'Negative values not supported';
                  return null;
                },
                onSaved: (value) => setState(() {
                  _count = value;
                }),
              ),
              FlatButton(
                child: Text('Submit'),
                color: Colors.blue,
                onPressed: () {
                  if (this._formKey.currentState.validate()) {
                    this._formKey.currentState.save();
                  }
                },
              ),
              SizedBox(height: 20.0),
              Text('Result = $_count')
            ]));
  }
}

// int型のデータを保有する独自FormLieldクラスを作成
class CounterFormField extends FormField<int> {
  CounterFormField(
      {FormFieldSetter<int> onSaved,
      FormFieldValidator<int> validator,
      int initialValue = 0,
      bool autovalidate = false})
      : super(
            onSaved: onSaved,
            validator: validator,
            initialValue: initialValue,
            autovalidate: autovalidate,
            builder: (FormFieldState<int> state) {
              return Column(children: <Widget>[
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    IconButton(
                      icon: Icon(Icons.remove),
                      onPressed: () {
                        // Fieldの値を変化させる
                        state.didChange(state.value - 1);
                      },
                    ),
                    Text(state.value.toString()),
                    IconButton(
                      icon: Icon(Icons.add),
                      onPressed: () {
                        state.didChange(state.value + 1);
                      },
                    ),
                  ],
                ),
                state.hasError // Validatorの結果がエラー時の表示をここで対応
                    ? Text(
                        state.errorText,
                        style: TextStyle(color: Colors.red),
                      )
                    : Container(),
              ]);
            });
}

RawKeyboardListener

RawKeyboardListenerクラス仕様

キーボード入力イベントをキャッチするためのWidgetです。iOS (かつエミュレータのみ?) 以外は問題なく動いてそうですが、iOS (かつエミュレータのみ?) だと反応が無いです。2016年の時点でissueが出ていますね。。
RawKeyboardListener doesn't receive any events from keyboard in ios simulator

サンプル

キーイベントを検出したら表示しているだけのサンプルです。
ezgif.com-video-to-gif-2.gif

サンプルコード
class RawKeyboardListenerSampleState extends State<WidgetSample> {
  final FocusNode _focusNode = FocusNode();
  String _message;

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }

  void _handleKeyEvent(RawKeyEvent event) {
    setState(() {
      _message =
          'Key: ${event.logicalKey.debugName}, KeyId: ${event.logicalKey.keyId}';
    });
  }

  @override
  Widget build(BuildContext context) {
    return RawKeyboardListener(
        focusNode: _focusNode,
        onKey: _handleKeyEvent,
        child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              TextField(),
              Text(_message ?? 'Press a key'),
            ]));
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS と Android で Opus 形式のオーディオを再生しようとしたらコンテナの違いにはまった話し

Android と iOS で Opus を扱うにあたって チョットワカッタ -> 完全に理解した(慢心)を20ループくらいしたので知見をメモします。

やりたかったこと

.wav(の中のPCM)音源を品質と圧縮率のバランス良い形のフォーマットに変換して、Android (android.media.MediaPlayer) と iOS (AVKit) からよしなに再生したい。オーディオファイルのフォーマットとしては Opus が良さそうでこれに目を付ける。

結論

Android 向けには Matroskaコンテナ、iOS 向けには CAFコンテナの中に libopus で圧縮した Opus 形式の音声データを格納すればよい。1ファイルで両プラットフォーム対応は無理なので諦めよう。

FFmpeg を使う場合以下のようになる。

# for Android
ffmpeg -i input.wav -vn -ac 2 -c:a libopus -b:a 24k output.mkv

# for iOS
ffmpeg -i input.wav -vn -ac 2 -c:a libopus -b:a 24k output.caf

input.wav が 140MB 程度で、エンコードしたあとは概ね 1MB程度。(上記コマンドのとおり ビットレートを 24k に指定した場合)

何が大変だったのか

Android は公式に Opus コーデック(のエンコード)をサポートしているが1、コンテナが適切でないと再生はできるけどシークが適切に行えない、などの問題があった。ローカルファイルだと行えてストリーミングだと無理、などの場合もあり問題の切り分けに苦労しつつ試行錯誤の末コンテナを Matroska にする必要がありそうなことに気付く(というか公式ドキュメントに書いてあった)。この間 .ogg, .oga, libopus, libvorbis などの組み合わせを無限に総当たりで試し続ける。

iOS は iOS11 から Opus コーデックをサポートするという話しがあったが結局どうなってしまったのかよくわかっていない。Androidでいけた .mkv ファイルがうまく再生できず詰んだ!ここは旧世代の遺物AACか〜 からの、afconvert コマンドで適当に .caf ファイルを作ってみたらなぜか再生できることに気づき、コンテナを CAF にする必要がありそうなことに気付く。

ちなみに Opus と CAF については Wikipedia - Opus を見ると (Limited container support) という表記とともに On iOS 11:Core Audio Format (.caf) と書いてあるのでつまりそういうことなんだろう。ということはこの記事を書いていて気付きました。アウトプット大事...。

何で Opus ?

音楽ファイルが96kbpsになる日――Opus音声コーデックの実力 - Qiita とか、YouTube で Opus vs MP3 で検索した結果 とかを参照すると良いと思います。感覚的な表現をすると、低ビットレートなMP3における音のこもり感が劇的に解消されている感じ。劇的に。これにより高品質で高圧縮なオーディオファイルをつくることができる。特に最新の技術ではない。

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

他のLiveDataをObserveできるMediatorLiveData

プロジェクト内でちょくちょくMediatorLiveDataを使っている場所があり、調べてみたのですが今一つ腑に落ちなかったので自分で触ってみました??‍?

今回書いたコードはGithubに挙げてるのでこちらも参照してみてください??‍♂️

公式のMediatorLiveDataのページが完結でわかりやすかったのですが、MediatorLiveDataは 他のLiveDataをObserveできるLiveData です。

例えば、EditTextに入力された文字を表示しつつ、文字数も表示する、といったケースで考えてみます。

MainViewModel.kt
val message: MutableLiveData<String> = MutableLiveData()
val count: MediatorLiveData<Int> = MediatorLiveData()

画面に表示するmessageは通常のMutableLiveDataで宣言しています。
このmessageを監視して、文字数をカウントするLiveDataを MediatorLiveData として宣言してみました。

countがmessageを監視する、と言うのは addSource を使ってこう書きます。

MainViewModel.kt
count.addSource(message) {
    val cnt = message.value?.length ?: 0
    count.postValue(cnt)
}
count.addSource(監視したい対象のLiveData) {
  // 監視したい対象のLiveDataが変更された時の処理
}

これで、messageが変更されたときに自動で文字数がカウントされて、countにpostValueされるようになります。messageの変更は通常のLiveDataを同じように、更新用の関数を用意してあげて、View側から呼んであげる感じです。

MainViewModel.kt
fun postMessage(message: String) {
    this.message.postValue(message)
}

MainViewModelの全体はこんな感じ。

MainViewModel.kt
package com.github.yasukotelin.mediatorlivedatasample.ui.main

import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    val message: MutableLiveData<String> = MutableLiveData()
    val count: MediatorLiveData<Int> = MediatorLiveData()

    init {
        count.value = 0

        count.addSource(message) {
            val cnt = message.value?.length ?: 0
            count.postValue(cnt)
        }
    }

    fun postMessage(message: String) {
        this.message.postValue(message)
    }
}

実際に動かすとこんな感じで、入力された文字がmessageに流れると自動でcountが変更されます!

ちなみに、MediatorLiveDataを使わないで、messageを更新してから自分でcountも更新すれば同じじゃないの??と思うかもしれませんが、それだとうまく行きません。

MainViewModel.kt
package com.github.yasukotelin.mediatorlivedatasample.ui.main

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    val message: MutableLiveData<String> = MutableLiveData()
    val count: MutableLiveData<Int> = MutableLiveData()

    fun postMessage(message: String) {
        this.message.postValue(message)
        updateCount()
    }

    private fun updateCount() {
        val cnt = message.value?.length ?: 0
        count.postValue(cnt)
    }
}

入力された文字数とカウントが合ってませんね。LiveDataの postValue はUIスレッドとは別スレッドで更新するメソッドです。そのため非同期で処理がされるため、countをmessageの値を使って更新しようと思うと同期が取れてなくて期待した動きにならないといったわけです。

MediatorLiveDataを使えば、postValueで値が変更されたに発火するイベントを定義できるということがわかりました??‍♂️

さらにもうちょっと遊んでみます。

入力されたmessageが6文字以上だったらOKで、それ以外はNGにするといったよくあるやつをMediatorLiveDataを使って書いてみます。

プロパティは新しくcompleteMessageをMediatorLiveDataで追加しただけです。

MainViewModel.kt
val message: MutableLiveData<String> = MutableLiveData()
val count: MediatorLiveData<Int> = MediatorLiveData()
val completeMessage: MediatorLiveData<String> = MediatorLiveData()

監視の仕方はこんな感じにしてみます。
View -> messageを更新!
count -> messageを監視。自動で文字数を更新
completeMessage -> countを監視。6文字以上ならOKのメッセージ。

MainViewModel.kt
private val isCompleted: Boolean
    get() = count.value ?: 0 >= REQUIREMENT_WORDS

init {
    count.value = 0

    count.addSource(message) {
        val cnt = message.value?.length ?: 0
        count.postValue(cnt)
    }
    completeMessage.addSource(count) {
        val msg = if (isCompleted) "Completed!" else "$REQUIREMENT_WORDS words required"
        completeMessage.postValue(msg)
    }
}

fun postMessage(message: String) {
    this.message.postValue(message)
}

companion object {
    private val REQUIREMENT_WORDS = 6
}

MediatorLiveDataを使うことでなかなかにシンプルに書くことができました!

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

Android Mali GPU の Vulkan 制約のメモ

https://vulkan.gpuinfo.org/

詳細は Sascha 先生謹製の Vulkan Caps Viewer でわかります.

Mali GPU(G-72)

  • maxVertexInputAttributeOffset : 2047
  • maxVertexInputBindingStride: 2048
  • maxVertexInputAttributes: 16
  • maxVertexInputBindings: 16

=> 頂点あたり, float で 512 個まで. Vertex input に頂点モーフ情報を入れる場合は, 50 個くらい(vertex, normal, tangent で 9 floats/target を想定)が限界.

  • maxUniformBufferRange: 65536

Uniform buffer にデータを格納する場合は, 64KB まで. それ以上は SSBO や image buffer を使う.

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

AndroidでRecyclerViewを使ってみた

目的

  • RecyclerViewを使ってみる。
    • 題材:カレンダー(Grid形式)

ポイント

  • RecyclerView.Adapterの下記のメソッドを実装するだけで、簡単に実装ができました。
    • getItemCount()
    • onCreateViewHolder(parent: ViewGroup, viewType: Int)
    • onBindViewHolder(holder: DateAdapterHolder, position: Int)

実装内容

ActivityのonCreateでレイアウトを読み込み、RecyclerViewを生成します。
LayoutManagerは、GridLayoutManagerを設定し、カラム数はカレンダーのため7にします。

RecyclerViewMainActivity.kt
class RecyclerViewMainActivity : AppCompatActivity() {
    private val SPAN_COUNT = 7

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view)

        val recyclerView = findViewById<RecyclerView>(R.id.calendar)
        val adapter = DateAdapter(this)
        recyclerView.adapter = adapter
        // GridLayoutManagerを設定
        // カレンダーなのでspanCount(カラム数)は7
        val layoutManager = GridLayoutManager(this, SPAN_COUNT)
        recyclerView.layoutManager = layoutManager
    }
}

レイアウトは単純に、画面全体にRecyclerViewを設定します。

activity_recycler_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.ykato.sample.kotlin.RecyclerViewMainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/calendar"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

続いては、Adapterです。

内部にViewHolderクラスを実装します。
今回Grid内には、日付用のTextViewを配置します。

getItemCount()の戻り値は、固定で700としました。
※dateInfoListのサイズ(可変)にしようと思いましたが、getItemCountの値を途中で変えると色々と問題があったのでここでは固定値としています。

DateAdapter.kt
class DateAdapter (
    context: Context) : RecyclerView.Adapter<DateAdapter.DateAdapterHolder>() {
    private val ITEM_COUNT = 700
    private val UPDATE_POSITION = 14
    private val inflater = LayoutInflater.from(context)
    private val dateInfoUtil = DateInfoUtil()
    private var dateInfoList = dateInfoUtil.createDateInfoList()

    class DateAdapterHolder(view: View) : RecyclerView.ViewHolder(view) {
        var date = view.findViewById<TextView>(R.id.date)
    }

    override fun getItemCount(): Int {
        return ITEM_COUNT
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DateAdapterHolder {
        val view = inflater.inflate(R.layout.calendar_item, parent, false)
        return DateAdapterHolder(view)
    }

    override fun onBindViewHolder(holder: DateAdapterHolder, position: Int) {
        holder.date.text = dateInfoList[position].dateString
        holder.date.setTextColor(dateInfoList[position].textColor)

        if (dateInfoUtil.getDateInfoSize() - position < UPDATE_POSITION) {
            addDateInfo()
        }
    }

    private fun addDateInfo() {
        dateInfoUtil.addDateInfo()
        dateInfoList = dateInfoUtil.createDateInfoList()
    }
}

onCreateViewHolder(parent: ViewGroup, viewType: Int)では、レイアウトを読み込み、ViewHolderを生成して返します。

onBindViewHolder(holder: DateAdapterHolder, position: Int)ではpositionに応じた文字列をTexViewに設定します。

calendar_item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/calenderItem"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="right|center_vertical">

    <TextView
        android:id="@+id/date"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="right"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="日付" />
</android.support.constraint.ConstraintLayout>

DateInfoUtilクラスは、カレンダーに表示する月、曜日、日付のリストを作成するクラスです。
下記のイメージの日付け等の情報を返すように実装しています。

1月
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
DateInfoUtil.kt
class DateInfoUtil {
    private val ADDITIONAL_COUNT_WEEK = 15
    private var localDate = LocalDate.of(LocalDate.now().year, LocalDate.now().month,1)
    private var dateInfoList = ArrayList<DateInfo>()
    private val emptyDateInfo = DateInfo("")
    private val dayOfWeekArray = arrayOf(DayOfWeek.SUNDAY,DayOfWeek.MONDAY,DayOfWeek.TUESDAY,DayOfWeek.WEDNESDAY,
            DayOfWeek.THURSDAY,DayOfWeek.FRIDAY,DayOfWeek.SATURDAY)

    private fun addMonthHeader(localDate: LocalDate , calenderStr : ArrayList<DateInfo>) {
        if (isJanuary(localDate)) {
            addYearToDateInfoList(calenderStr, localDate)
            addMonthToDateInfoList(calenderStr, localDate)
        } else {
            addMonthToDateInfoList(calenderStr, localDate)
            addEmptyInfoToDateInfoList(calenderStr)
        }
        addEmptyInfoToDateInfoList(calenderStr, 5)
    }

    private fun isJanuary(localDate: LocalDate) = localDate.month == Month.JANUARY

    private fun addYearToDateInfoList(calenderStr: ArrayList<DateInfo>, localDate: LocalDate) {
        calenderStr.add(DateInfo(localDate.year.toString()))
    }

    private fun addMonthToDateInfoList(calenderStr: ArrayList<DateInfo>, localDate: LocalDate) {
        calenderStr.add(DateInfo(localDate.month.toString().substring(0, 3)))
    }

    private fun addEmptyInfoToDateInfoList(calenderStr: ArrayList<DateInfo>, count: Int = 1) {
        for (i in 1..count) {
            calenderStr.add(emptyDateInfo)
        }
    }

    private fun addWeekHeader(calenderStr : ArrayList<DateInfo>) {
        for (i in dayOfWeekArray) {
            val dateInfo = DateInfo(i.toString().substring(0, 3))
            dateInfo.textColor = when(i) {
                DayOfWeek.SUNDAY -> Color.RED
                DayOfWeek.SATURDAY -> Color.BLUE
                else -> Color.BLACK
            }
            calenderStr.add(dateInfo)
        }
    }

    private fun addDateInfo(calenderStr : ArrayList<DateInfo>) {
        var isFirstDay = localDate.dayOfMonth == 1
        for (i in 1..ADDITIONAL_COUNT_WEEK) {
            if (isFirstDay) {
                addMonthHeader(localDate, calenderStr)
                addWeekHeader(calenderStr)
                isFirstDay = false
            }
            for (j in dayOfWeekArray) {
                if (!isFirstDay && j == localDate.dayOfWeek) {
                    val dateInfo = DateInfo(localDate.dayOfMonth.toString())
                    dateInfo.textColor = when(j) {
                        DayOfWeek.SUNDAY -> Color.RED
                        DayOfWeek.SATURDAY -> Color.BLUE
                        else -> Color.BLACK
                    }
                    calenderStr.add(dateInfo)
                    localDate = localDate.plusDays(1.toLong())
                    if (localDate.dayOfMonth == 1) {
                        isFirstDay = true
                    }
                } else {
                    calenderStr.add(emptyDateInfo)
                }
            }
        }
    }

    fun createDateInfoList() : ArrayList<DateInfo> {
        addDateInfo(dateInfoList)
        return dateInfoList
    }

    fun getDateInfoSize() : Int {
        return dateInfoList.size
    }

    fun addDateInfo() {
        addDateInfo(dateInfoList)
    }
}

改善点

  • 表示までのパフォーマンスが良くないので原因調査と改善が必要です。
  • 月と曜日はスクロールしても残るように、ItemDecorationを使用してスティッキーヘッダーに修正します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]LeakCanaryによるメモリリークの検知

Androidアプリにおけるメモリリーク

Androidアプリ開発をしている上で、メモリリークって怖いですよね。
「Contextの扱いを誤るとメモリリークするよ!」って言われても
「そもそもContextってなんやねん!」って思われている方も多いのではないでしょうか。

LeakCanary

開発中のアプリでRecyclerViewを使っていて、こちらの記事を拝見しました。
たまたま開発中のアプリでも同様の実装をしており、メモリリークが身近なものであるということを再認識したので
上記の記事でも紹介されているLeakCanaryを導入してみました。

導入

appレベルのbuild.gradleに依存関係を追加するだけです。
古いバージョンではちょこっとソースコードを書く必要があったようですが、
最新バージョンではソースコードを書く必要すらありません。

dependencies {
    def leakcanary_version = "2.2"

    // LeakCanary
    debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"

}

使い方

アプリ実行中するだけ。
メモリリークを検知するとアプリが通知してくれるようになります。
Screenshot_1582356636.png

通知をタップすると、より詳細なダンプを確認できます。
めっちゃ便利。

参考

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

【Android】RecyclerView+DataBindingでリストを作ったときに初期位置が読み込まれない

※初投稿です
※Android初学者の備忘録です
※原因はわかり次第追記予定

表題の結論

XMLで

            android:text="@{model.name}"

のように指定すると、リストを表示したときに最初の1ページ分が表示されないことがある
=指定をなくすと表示される
(原因はよくわかってないです)

発生した問題

RecyclerView + DataBinding + Moshiを使って、以下のようなアプリを作成しています

キャプチャ.PNG

↑ 自作のJsonを読み込んでリストに起こしてます

ただし、XMLで@{model.name}みたいなかたちで指定すると

キャプチャ2.PNG

↑ のように最初の1ページ分が表示されません
(十分下までスクロールして戻ってくると表示される)

なぜこうなるのか?

RecyclerViewに問題があるのか、DataBindingに問題があるのか、はたまたMoshiに問題があるのか
そこがイマイチよくわからず、結局どこに原因があるのかはわかりませんでした

ただ、別の記事などを参考にするとXMLで指定をして上手く動作していることが多いので、
重いデータを読み込んでいる場合に起こるのだと思います
(でも以前WebAPIから読み込んだときはうまいことできてたし…?)

原因は分かり次第更新しようと思います

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

FlutterのAndroidビルドで警告が発生したのでminSdkVersionを上げて解決した話

TerminalでAndroidビルドの実行

Flutterでアプリ開発していて、しばらくはiPhoneしか使っていなかったのですが、Pixel 3 XL を購入したのでAndroidでも実機実行しようと思いました。
とりあえずVS CodeのTerminalでAPK Buildすると…

% flutter build apk

警告発生

以下のエラーが赤く表示されました…
動きはするんですが気持ち悪いですよね?

注意:/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_admob-0.9.0+10/android/src/main/java/io/flutter/plugins/firebaseadmob/AdRequestBuilderFactory.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
注意:/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_core-0.4.3+1/android/src/main/java/io/flutter/plugins/firebase/core/FirebaseCorePlugin.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
注意:/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-0.13.0+1/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.javaの操作は、未チェックまたは安全ではありません。
注意:詳細は、-Xlint:uncheckedオプションを指定して再コンパイルしてください。
注意:/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/firebase_analytics-5.0.9/android/src/main/java/io/flutter/plugins/firebaseanalytics/FirebaseAnalyticsPlugin.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
注意:/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/package_info-0.4.0+13/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
/Users/riscait/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.6/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java:25: 警告: [deprecation] FlutterPluginBindingのgetFlutterEngine()は非推奨になりました
    setupChannel(binding.getFlutterEngine().getDartExecutor(), binding.getApplicationContext());
                        ^                                               
警告1個                                                                 
注意:一部の入力ファイルは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
  • Firebase AdMob
  • Firebase Core
  • Firebase Cloud Firestore
  • Firebase Analytics
  • PackageInfo
  • SharedPlugin

のプラグインで警告が発生しているようですね。

解決方法

/android/app/build.gradle ファイルを開き、1行編集します。

defaultConfig {
    applicationId "com.company.sample"
    minSdkVersion 23 // <-ここを16だったので23に上げました
    targetSdkVersion 28

minSdkVersionを上げました。
最初は21に上げたのですが、警告に変化なかったので23に上げたらすべて解決しました!

ライブラリによって設定すべきminSdkVersionがあるんでしょうね。
どこかに書いてあるのかもしれませんが、軽く見た感じでは見つけることができませんでした?

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