20200325のAndroidに関する記事は9件です。

Flutter で外部ストレージにアクセスするアプリを作るときは Android バージョンを考慮して『ひと手間』いるから注意してくれよな!

TL;DR

READ_EXTERNAL_STORAGE パーミッションや WRITE_EXTERNAL_STORAGE パーミッションを必要としているアプリは、 AndroidManifest.xmlandroid:requestLegacyExternalStorage="true" を設定すれば OK です。

app/src/main/AndroidManifest.xml
<application
    android:name="io.flutter.app.FlutterApplication"
    android:label="YourAppName"
    android:icon="@mipmap/ic_launcher"
    android:requestLegacyExternalStorage="true">

事の発端

開発中のアプリで image_picker を使っていました。
エミュレーターでは問題なく動作していたのですが、いざ実機で動作確認をしてみると……

「イメージをピックできない!?」

となったわけです。

実はエラーが発生していた

デバッグして確認してみると、

Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/Pictures/any_image.jpg: open failed: EACCES (Permission denied)

というエラーが発生していました。

パーミッション? と思い AndroidManifest.xml<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> を設定してみましたが結果は変わりませんでした。

原因

じつは Issues にあがっていました。
Android バージョンが原因のようです。

変更されていた外部ストレージへのアクセス方法

Android 10 ( API レベル 29 )以降から外部ストレージへのアクセス管理が変更 1 になっているようです。

ユーザーがファイルを詳細に管理して整理できるように、Android 10(API レベル 29)以降をターゲットとするアプリには、外部ストレージ デバイスに対する特別アクセス権限がデフォルトで付与されます(対象範囲別ストレージ)

とのことで、 READ_EXTERNAL_STORAGE パーミッションや WRITE_EXTERNAL_STORAGE パーミッションは不要になっています。

また、他のアプリが作成したファイルへのアクセスは、

  1. アプリに READ_EXTERNAL_STORAGE パーミッションが付与されていること。
  2. 対象ファイルが、明確に定義された次のいずれかのメディア コレクション内にあること。

のようになっています。

つまり、 Flutter のパッケージがこの対応についていっていないと外部ストレージにアクセスできない状況が発生するということです。

対策

Android デベロッパー に次のような警告があがっていました。
警告.png
つまり、いずれきれいにしたいけど今は微妙な状態だから気をつけてな! ってことでしょうね。
だから上で紹介した Issues もオープン状態のままなわけで……。

そんな状態でも一応は対処方法があるようなので紹介します。

対象範囲別ストレージをオプトアウトする

Android 9( API レベル 28 )以前をターゲットにする

これはそのとおりですね。
アクセス管理変更前のバージョンを相手にすればエラーは起こりません。

requestLegacyExternalStorage の値を true に設定する

Android 10 以降をターゲットにしている場合は、 AndroidManifest.xmlandroid:requestLegacyExternalStorage="true" を設定すれば OK です。

app/src/main/AndroidManifest.xml
<application
    android:name="io.flutter.app.FlutterApplication"
    android:label="YourAppName"
    android:icon="@mipmap/ic_launcher"
    android:requestLegacyExternalStorage="true">

両方のバージョンをターゲットにしたい場合は?

app/src/main/AndroidManifest.xml
<uses-permission 
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="18" />

uses-permissionmaxSdkVersion 属性が追加されたようなので、 18 を指定すれば良さそうです。
(ただ Flutter で利用している分には maxSdkVersion 属性を指定してなくても動きそうなんだよな…… :thinking::question:

まとめ

Flutter の開発をしていると、エラーの解消に割と時間がかかります。
プラットフォーム依存のものが多かったり、そもそも情報が少ないのも理由かもしれません。

自分が遭遇したエラーで情報の少ないものはなるべく書き溜めていこうと思いますので、同じエラーで困っている人のお役に少しでも立てれば幸いです!


  1. 変更内容の詳細は Android デベロッパー に詳しく記載されています 

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

Roomのリレーションで悩んでいたら、リレーション実現しなくても実装出来た

Roomで1対1対応のリレーションを実現したい

Roomを使ったアプリケーションで、別々のテーブルの情報が両方必要なので、昔Railsを触った自分としては、

「ついにAndroidのRoomでもリレーションを実現しなければならない時が来た」

と思いました。
そこでドキュメントなど、色々見ていたのですがRoomで1対1対応のリレーションは、ズバリこう実装すれば良いというのがイマイチ分からないでいました。
ActiveRecordでいうhas_oneのような物は無いのでしょうか。

ところが「参照するだけなら、そもそもRoomでリレーション組まなくても実装できる」という話を聞いて、自分は最初よく理解出来ませんでした。

DBでリレーション使わず、実現した方法

今回はサンプルとして、仮にゲームのQuestionテーブルとScoreテーブルとします。

// データモデルQuestion
id: Int
level: Int
number: Int
question: String
created_at: String
// データモデルScore
id: Int
level: Int
number: Int
time: Int
score: Int
clear_date: String

この2つを関連づけたいとします。このQuestionテーブルに対応したScoreを結び付けたい。必要な情報を抽出して、

// QuestionとScoreをまとめて扱う、QuestionAndScoreクラス

class QuestionAndScore {
    companion object {
        fun createQuestionAndScore(level: Int, number: Int, question: String): QuestionAndScore {
            return QuestionAndScore().apply {
                this.level = level
                this.number = number
                this.question = question
            }
        }
    }
    var level: Int = 0
    var number: Int = 0
    var question: String = ""
    var time: String = ""
}

2つのテーブルの情報を同時に扱うQuestionAndScoreクラスを作成しました。

  • 今回はQuestion情報に対する最高スコアを組み合わせた問題リストを作成することにします。
val allQuestions = getAllQuestions() //全ての問題データを取得する
val allScores = getAllScores()  //全てのスコアデータ取得

// 問題データとスコアデータを組み合わせたリスト
val allQuestionsAndScores = mutableListOf<QuestionAndScore>().apply {
    allQuestions.forEach { questionData ->
        val level = questionData.level
        val number = questionData.number
        val question = questionData.question
        val questionAndScore = createQuestionAndScore(level, number, questoin)
        // 関連するスコアだけを取り出す
        val scoreList = allScores.filter { scoreData ->
            scoreData.level == level && scoreData.number == number
        }
        // 最高スコアを取得
        val maxScore = scoreList.maxby {
            it.score
        }!!.score
        questionAndScore.score = maxTime
        add(questionAndScore)
    }
}

これで問題情報と各問題の最高スコアを組み合わせた問題リストが作成出来ました。

値を参照するだけなら、このように実現することが出来ました。

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

FlutterでWidgetの位置とサイズを取得する

FlutterでWidgetの位置やサイズを取得したい

検索しても地図上の位置を取得するのばっかヒットしてつらい

GlobalKeyを使ってRenderBoxを取得する

参考サイト:https://medium.com/@diegoveloper/flutter-widget-size-and-position-b0a9ffed9407

TestState.dart
//class TestWidgetは省略

GlobaleKey globaleKey = GlobalKey(); //←これが重要

class _TestState extends State<TestWidget>{
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Expanded(
          child: RaisedButton(
            child: Text("test"),
            onPressed: (){
               //↓変数はRenderBoxで宣言(.findRenderObject()で帰ってくるのは"RenderObject"のため
              RenderBox box = globalKey.currentContext.findRenderObject();
              print("ウィジェットのサイズ :${box.size}");
              print("ウィジェットの位置 :${box.localToGlobal(Offset.zero)}");
            },
          ),
        ),
        Center(
          child: Text("このウィジェットのサイズ",
            key: globalKey, //←知りたいWidgetにGlobalKeyをセット
          ),
        ),
      ],
    );
  }
}

RenderBox box = globalKey.currentContext.findRenderObject();

これでGlobalKeyをセットしたWidgetを元に描画されたRenderBoxインスタンスを取得出来ます。(返ってくるのはRenderObjectなので変数の宣言はRenderBoxにする)

結果

ウィジェットのサイズ :Size(88.0, 48.0)
ウィジェットの位置 :Offset(0.0,24.0)

.localeToGlobal(Offset)で取得している位置は、ウィジェットの左上の点
引数のOffsetがゼロでない場合、その分の座標が足される

注意点

GlobalKeyを付けたWidgetが一度もBuildされていない場合、RenderBoxは取得出来ない
一度もWidgetがbuildされていない場合、RenderBoxはそもそも描画されていないので取得出来ません。(大きさが可変のWidgetを考えてみれば分かる)

//失敗するやり方

GlobaleKey globaleKey = GlobaleKey();
class _TestState extends State<TestWidget> {
  //サイズと位置を取得するメソッド
  String _getLocaleAndSize() {
    RenderBox box = globalKey.currentContext.findRenderObject();
    return "ウィジェットのサイズ :${box.size}\n"
        "ウィジェットの位置 :${box.localToGlobal(Offset.zero)}";
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Expanded(
          child: RaisedButton(
            child: Text(_getLocaleAndSize()),//←ボタンのタイトルにする
            onPressed: () {
              print(_getLocaleAndSize);
            },
          ),
        ),
        Center(
          child: Text(
            "このウィジェットのサイズ",
            key: globalKey,
          ),
        ),
      ],
    );
  }
}

結果

エラー

The method 'findRenderObject' was called on null.
Receiver: null
Tried calling: findRenderObject()

回避法

WidgetsBinding.instance.addPostFrameCallback(Function callback)を使う
WidgetsBinding.addPostFrameCallbackを使うとBuild終了時に実行する処理が書けます

var globalKey = GlobalKey();

class _TestState extends State<TestWidget> {
  String _getLocaleAndSize() {
    RenderBox box = globalKey.currentContext.findRenderObject();
    return "ウィジェットのサイズ :${box.size}\n"
        "ウィジェットの位置 :${box.localToGlobal(Offset.zero)}";
  }
  String _text;//←変数を用意
  @override
  Widget build(BuildContext context) {
    if (_text == null)//Build時、テキストがnullの場合↓を実行
      WidgetsBinding.instance.addPostFrameCallback((cb){
        setState(() {
          _text = _getLocaleAndSize();
        });
      });
    return Column(
      children: <Widget>[
        Expanded(
          child: RaisedButton(
            child: Text(_text ?? "テキストはまだない"),
            onPressed: () {
              print(_getLocaleAndSize);
            },
          ),
        ),
        Center(
          child: Text(
            "このウィジェットのサイズ",
            key: globalKey,
          ),
        ),
      ],
    );
  }
}

注意
WidgetsBinding.addPostFrameCallbackは他にも色々使えて便利ですが、build時に使用するときは無限ループしないように気をつけましょうね~

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

【Android】Android 11から始めるIME Transitions

IME Transitionsとは

Android 11 DP2から追加されたWindowInsets APIの一つです。
https://android-developers.googleblog.com/2020/03/android-11-developer-preview-2.html

ime_transition.gif

さっくり作ってみた

MainActivity.kt
import android.graphics.Insets
import android.os.Bundle
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity() {

    private val spacing36 by lazy {
        applicationContext.resources.getDimensionPixelOffset(R.dimen.spacing_36)
    }

    private val spacing16 by lazy {
        applicationContext.resources.getDimensionPixelOffset(R.dimen.spacing_16)
    }

    private val listener = object: WindowInsetsAnimationControlListener {
        override fun onCancelled() {
            animationController = null
            isFirst = true
        }

        override fun onReady(controller: WindowInsetsAnimationController, types: Int) {
            animationController = controller
        }
    }

    private var animationController: WindowInsetsAnimationController? = null

    private var isFirst = true

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

        window.setDecorFitsSystemWindows(false)

        val callback = object: WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
            override fun onProgress(
                insets: WindowInsets,
                animations: MutableList<WindowInsetsAnimation>
            ): WindowInsets {
                text_input_layout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                    updateMargins(bottom = insets.getInsets(WindowInsets.Type.ime()).bottom + spacing16)
                }
                return insets
            }
        }
        text_input_layout.setWindowInsetsAnimationCallback(callback)

        scroll_view.setOnScrollChangeListener { view, x, y, oldX, oldY ->
            if (isFirst && y != 0) {
                isFirst = false
                text_input_layout.windowInsetsController?.controlWindowInsetsAnimation(
                    WindowInsets.Type.ime(),
                    -1,
                    null,
                    listener
                )
            }
            animationController?.setInsetsAndAlpha(Insets.of(0, 0, 0, scroll_view.scrollY - spacing36), 1f, 0f)
        }
    }
}

レイアウトファイルはScrollViewのなかにTextInputLayoutを追加すれば動く。

所感

その他にも、inputLayout.windowInsetsController.hide() or show()でキーボードの表示切り替えができます。
また、サンプルコードではTextInputLayoutにFocusが当たってるときと当たっていない時の制御を入れてないので、そこらへんの実装をちゃんとすればより良いUIになりそうです。

※DP2のため今後変更の可能性があります。

参考

https://android-developers.googleblog.com/2020/03/android-11-developer-preview-2.html
https://developer.android.com/reference/kotlin/android/view/WindowInsetsAnimation.Callback
https://developer.android.com/reference/android/view/WindowInsetsAnimationController

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

FlutterでSplashスクリーンを設定する

SplashScreenとは?

アプリを起動させた時にアイコンが中央に表示される画面のことです。
これがあるだけで、大分アプリっぽくなりますよね?
ダウンロード.gif
実装にはiOSとAndroidで異なる手順を踏まなくてはいけなくなるので、
それぞれについて説明していきます。

iOS

iOSは比較的簡単な手順で済みます。
1. project/ios/Runner/Assets.xcassets/LaunchImage.imageset/配下に画像を保存
2. project/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.jsonを編集
以上の2つのみです。

画像を保存

この時、画像は1x, 2x, 3xの3つの倍率を用意しなければいけません。
カスタムで4xの倍率を用意してもいいみたいです。

Contents.jsonの編集

Contents.jsonを以下のコードに書き換えます。

{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "画像ファイル名.png",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "filename" : "画像ファイル名@2x.png",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "filename" : "画像ファイル名@3x.png",
      "scale" : "3x"
    },
    {
      "idiom" : "universal",
      "filename" : "画像ファイル名@4x.png",
      "scale" : "4x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

Android

Androidは結構苦戦しました?
その時遭遇したエラー対処方法なども一緒に載せておくので、合わせてみていただけると
問題なくできると思います。
1. project/android/app/src/main/res/配下に画像を保存
2. project/android/app/src/main/res/values/styles.xmlを編集
3. project/android/app/src/main/res/drawable/launch_backgound.xmlに追加
以上の3つの手順になります。

画像を保存する

AndroidはiOSと異なり、hdpi,mdpi,xhdpi,xxhdpi,xxxhdpiの5つの倍率を用意しなければいけません。
また、{フォルダ名}-hdpi/{画像ファイル名} のように、それぞれの倍率をディレクトリごとで分けなければいけないので少し面倒です?
ここで画像ファイル名を決める時に、大文字-(ハイフン)は使用できないので注意してください!
a-zと0-9と_(アンダースコア)のみになりますので、iOSの方で画像名に大文字を使用している場合は変更が必要になります。

styles.xmlを編集

styles.xmlを以下のコードに書き換えます。
カラーコードが書かれているところは背景色の色なので、好きな色に変更することができます。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <item name="android:windowBackground">@フォルダ名/launch_background</item>
    </style>
    <!-- #FFFFFFはカスタム可能です-->
    <color name="background">#FFFFFF</color>
</resources>

launch_background.xmlを編集

android:drawableを@color/backgroudに置き換えます。
これによりstyles.xmlで指定した背景色に変更することができます。
また、新たにitemタグを追加してください。

...
<item android:drawable="@color/background"/>
<item
        android:drawable="@フォルダ名/画像ファイル名"
        android:gravity="center" />
...

エラー対処

adb: failed to install /Users//Desktop/flutter_sample/build/app/outputs/apk/app.apk: Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE] Error launching application on Android SDK built for x86.

実行しようとすると上記のエラーが出て、デバッグできないようなことがありました。
これは、エミュレータのStorageがいっぱいになっているのが原因でした。
- エミュレータの不要なアプリを消す
- 新規のエミュレータを作成する
このどちらかを行えば解決しました。
SplashScreenとは関係のないエラーですが、参考までに記述しておきます。

Splashスクリーンの表示アイコンが大きすぎる

もう一つはAndroidのみで起きた現象ですが、用意したアイコンが大きすぎたのか、画面いっぱいにアイコンが表示されてしまいました。
この対処にはlaunch_background.xmlを追加で編集する必要があります。
先ほど追加したitemタグの要素にwidthとheightを指定してあげるといい感じに修正できました?

<item
        android:width="200dp" <--追加
        android:height="200dp" <--追加
        android:drawable="@mipmap/launch_splash"
        android:gravity="center" />

まとめ

Androidでやけに苦戦しまいましたが、なんとか実装ができました!
次回はアプリアイコンについてやろうかなと思います?
誰かのお役に立てたら嬉しいです?✨
それではまた!

参考文献

https://qiita.com/shinki_uei/items/c0b9b9a6d25e280c7bec
https://www.developerlibs.com/2018/07/flutter-how-to-fix-white-screen-on-app.html
https://miajimyu.hatenablog.com/entry/2019/09/30/212723

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

【初心者向け】スマホでPCの画面を見る方法【iPhone/Android】

パソコン画面をiPhone/Androidに表示する最適な方法

iPhone/Androidの画面をパソコンにミラーリングして表示させる方法を知っている人が多いようですが、
パソコンをスマホにミラーリングする方法を知る人は多くではありません。

では、この記事はパソコン画面をiPhone/Androidに表示する最適な方法について、ご紹介します。

事前準備

iPhone/Androidスマホ(この記事ではiPhoneを例として説明します。)
Windows PC(Windows7/8/8.1/10)
画面ミラーリングアプリ(この記事ではLetsView という無料のアプリを例として操作します。ApowerMirrorというミラーリングアプリもおすすめです。)

操作手順

1.スマホとパソコンを同じWi-Fiネットワークに接続しておきます。
2.スマホとパソコンにLetsView をダウンロードして、起動します。(両方ともインストールしてください。)
LetsViewダウンロード
3.iPhone側で検出されたデバイスのリストが見えます。
letsview画面ミラーリング.png
※デバイスが検出されていない場合、「再検出」をタップします。
4.お使いのパソコンを選択します。
デバイス検出.png
5.「PC画面ミラーリング」を選択します。
image.png
6.パソコン側から「許可」を選択します。
image.png

上記の手順に従うだけで、パソコンの画面をiPhoneにミラーリングして表示させることができます。

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

【Android】画面の表示サイズ(dpi)変更に耐えられるLayoutを定義する その2

AutosizingTextView について

AutosizingTextView の詳細については以前投稿したもを参考にしてください
画面の表示サイズ(dpi)変更に耐えられるLayoutを定義する - Qiita

AutosizingTextView を APIレベル 26 未満で使う

AutosizingTextView は Android 8.0(API レベル 26)以降で使用できるようになった機能である
今回の目的は APIレベル 26 未満でも AutosizingTextView の自動サイズが適用される Layout を投稿することである
APIレベル 26 未満でも簡単に使用できたのでメモしておく

普通の TextView でのレイアウト崩れの例

・3行に改行されてしまうTextViewがあったとする
・ここの文字列は可変であり何がくるかわからないとする(3文字くらいの単語が入ることも想定)
・短い単語では表示に問題はない
・横幅は変えれないような仕様となっている
・長い文字列の時に表示が崩れたくない要望がある

どんな文字列が来ても表示が大丈夫なように作っておきたい

layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/view1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/view1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="Hello World"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/button2"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@id/button1"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toEndOf="@id/view1"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

NG layout.png

AutosizingTextView で解決

AutosizingTextView を使うといい感じに1行で自動的に収まるようになる

layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/view1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/view1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:autoSizeMaxTextSize="20sp"
        android:autoSizeMinTextSize="1dp"
        android:autoSizeTextType="uniform"
        android:gravity="center_horizontal"
        android:maxLines="1"
        android:text="Hello World"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/button2"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@id/button1"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toEndOf="@id/view1"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

OK layout.png

APIレベル 26 未満では適用されない

上のレイアウトと同じ実装で Andord 6 といった APIレベル 26 未満のOSでアプリを見てみるとリサイズされていない
Android 6 や 7 のシェアもまあまあ高いのでなんとかしたい

NG layout.png

Support Library で解決

Support Library を使うだけで下位のOSでも自動でリサイズできるようになる
今は AndroidX に Support Library が含まれるので AndroidX で定義してあげたほうが望ましい
https://developer.android.com/topic/libraries/support-library

layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/view1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- android.support.v7.widget.AppCompatTextView -->
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/view1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:maxLines="1"
        android:text="Hello World"
        android:textSize="20sp"
        app:autoSizeMaxTextSize="20sp"
        app:autoSizeMinTextSize="1dp"
        app:autoSizeTextType="uniform"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/button2"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@id/button1"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="4"
        app:layout_constraintStart_toEndOf="@id/view1"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

※ 「android:」 で定義されているところを 「app:」 に変えないとリサイズ効果が出ないので注意すること

これでOK
OK layout.png

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

Scratch filesで気軽にコードの実行結果を確認する

Scratch filesとは

手軽にKotlinやJavaなどを実行できるファイルだよ。
用途的に近いものとしてpaiza.io等のオンラインエディタだけど、モジュール内で定義したクラスやコード補完を使えるといった違いがあるよ。

Scratch filesの特徴

  • モジュール内で定義したクラスを使用できる
  • Interactive modeでコードを書き換えても即反映してくれる
  • コード補完がある
  • プロジェクトフォルダと違う場所にファイルが作成されるのでGitに追加されない

Scratch Fileの作成

  1. New -> Scratch Fileを選択
  2. Kotlinを選択

スクリーンショット 2020-03-25 11.19.16.png

作成すると以下の画面が出ていると思います。
スクリーンショット 2020-03-25 11.27.09.png

Scratch Fileの使い方

適当に処理を書いて Run Scratch File を押すと、右側に処理の実行結果が表示されます。(Interactive modeがONになっていると2秒毎に処理が実行される)
スクリーンショット 2020-03-25 11.33.13.png

Use classpath of moduleappに選択するとモジュール内で作成したクラスが使用できるようになります。
スクリーンショット 2020-03-25 12.50.00.png

おわりに

今までオンラインエディタでコードを試していたのですが、モジュール内で定義したクラスやコード補完がしっかりしている点でScratch files良きだなと思いました。

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

Retrofit2で共通処理を行う

AndroidアプリでAPIを叩きたい時、定番のライブラリと言えばRetrofitです。
このRetrofitを使って、APIからのレスポンスによって同じ様な処理を行う場合、毎回同じコードを書くのは面倒ですよね!
同じ様な処理は共通化しちゃいましょう!

通常のAPI呼び出し

例えば、エラー時に下記のようなToastを表示する仕組みがあったとします。
様々な箇所でAPIを呼び出す度、全てに同じ様な処理を書くのは効率が悪いです。
ここを上手く解決しましょう!

Api.getUser().enqueue(object: Callback<Response?> {
  override fun onResponse(call: Call<Response?>, response: Response<Response?>) {
    if(!response.isSuccessful) {
      // エラー時の処理
      Toast.makeText(context, "error!", Toast.LENGTH_SHORT).show()
    }
  }

  override fun onFailure(call: Call<Response?>, t: Throwable) {
  }
})

Callbackのオーバーライド

Callback クラスを継承したクラスを作成し、 onResponseonFailure をオーバーライドすることで、共通の処理を追加することができます。

open class CustomCallback<T>(private val context: Context): Callback<T> {
  override fun onResponse(call: Call<T>, response: Response<T>) {
    if(!response.isSuccessful) {
      // エラー時の共通処理
      Toast.makeText(context, "error!", Toast.LENGTH_SHORT).show()
    }
  }

  override fun onFailure(call: Call<T>, t: Throwable) {
  }
}

この様なクラスを作る事により、すべてのAPI呼び出し処理に共通処理を追加可能です。
下記のように使用します。
Toastの処理は書いていませんが、 super.onResponse(call, response) で元の処理を呼び出しているので、Toastが表示されます。
もちろん、 onFailure の箇所にも自由に追加可能です。

Api.getUser().enqueue(object: CustomCallback<Response?>(this) {
  override fun onResponse(call: Call<Response?>, response: Response<Response?>) {
    super.onResponse(call, response)
  }

  override fun onFailure(call: Call<Response?>, t: Throwable) {
    super.onFailure(call, t)
  }
})

簡単に共通処理を追加できるので、おすすめです!

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