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

カスタムビュー:ToolBar(ActionBar)を自作しよう!

ToolBar/ActionBarとは? スマホアプリを開くと大体上に表示されている、画像の青の丸で囲っている 部分です。ちなみにこのアプリの画面は適当に「スマホアプリ ツールバー」とかで調べました。また、ToolBarと呼ばれることもありますが、ActionBarとも呼ばれます。違いはよく分かりませんので私は同じであると認識しています。 なぜ自作するのか 実はわざわざ作らなくてもアプリをビルドすればデフォルトのツールバーが画面最上部に自動的に表示されます。もちろんそれで問題ない場合は既存のものを利用すれば良いと思います。ただ、デフォルトのアクションバーはかなり融通が効かず、もっとこういうふうにしたい!と思ってもなかなかうまく行きません。例えば、アクションバーに表示するタイトルを中央に表示したいなど、結構カンタンそうですが難しいです。他にもできそうでなかなかできないことが多いので、それなら自分で自分好みのツールバーを作っちゃおうということです。 それでは早速作っていきましょう ツールバーを作成 まずツールバーを作ります。私はこんな感じで左右にイメージボタン、中央にテキストビューを配置していかにもツールバーって感じのデザインにしてみました。 コードは以下の通りです custom_tool_bar.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/layout_custom_toolbar" android:layout_width="match_parent" android:layout_height="120dp" android:background="#76ead7" android:orientation="horizontal"> <ImageButton android:id="@+id/btn_left" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="0.5" android:background="@color/dark_green" android:scaleType="center" android:scaleX="2" android:scaleY="2" app:srcCompat="@drawable/ic_baseline_arrow_back_24" app:tint="@color/light_green" /> <TextView android:id="@+id/text_title" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/dark_green" android:gravity="center" android:text="アンチエイジング検診アプリ" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textColor="#76ead7" android:textSize="36sp" /> <ImageButton android:id="@+id/btn_right" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="0.5" android:background="@color/dark_green" android:scaleX="2" android:scaleY="2" app:srcCompat="@drawable/ic_baseline_menu_24" android:contentDescription="TODO" app:tint="@color/light_green" /> </LinearLayout> ではこのツールバーを配置したい画面に、ツールバーをいれる用の入れ物(コンテナ)を作成しましょう。 ツールバーを表示させたい画面に準備をする こんな感じで画面の上部にツールバーを表示するための空間をつくります。先ほどツールバーを作成した時はwidthはmatch_parentでheightは120dpにしました(もしかしたら120dpはちょっと大きめかもですね。これはタブレットなので少し太めにしてますが、普通のスマホアプリだとしたら100dpで十分かもしれません)。なので、同じサイズのLinearLayoutをツールバーの入れ物として表示させたい部分に作成します。 念のためにコードを貼っておきます。ちなみに今回はカスタムツールバーの説明なので画像のeditTextや送信ボタン部分のコードは無視してもらって大丈夫です! activity_input_data.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" android:background="@color/background_color" tools:context=".InputDataActivity"> <LinearLayout android:id="@+id/container_for_toolbar" android:layout_width="match_parent" android:layout_height="120dp" android:orientation="vertical" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"></LinearLayout> <LinearLayout android:layout_width="400dp" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <EditText android:id="@+id/edit_text_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="名前" android:inputType="textPersonName" android:textSize="24sp" /> <EditText android:id="@+id/edit_text_height" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="身長" android:inputType="textPersonName" android:textSize="24sp" /> <EditText android:id="@+id/edit_text_weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="体重" android:inputType="textPersonName" android:textSize="24sp" /> <Button android:id="@+id/btn_send" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" android:text="送信する" android:textColor="#393e46" android:textSize="24sp" android:textStyle="bold" app:backgroundTint="@color/light_green" app:rippleColor="@color/dark_green" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> カスタムツールバーが各画面で柔軟にデザインを変更できるように設定する 例えば、今回の場合はツールバーにイメージボタンやテキストビューがありますが、テキストビューに表示する文字を画面によって変えたいと思いませんか?もしくは、イメージボタンの画像も画面によっては非表示にしたり、または別の画像に変えたりしたくはありませんか?そういったことを可能にするための設定を行います。特に今回は、 イメージボタンをクリックした際の処理を、各画面で決定できるようにする イメージボタンを表示するか非表示にするかの切り替えを、各画面で決定できるようにする テキストビューに表示する文字を、各画面で決定できるようにする の、3点を実装します。もしイメージボタンの画像を各画面で決定できるようにしたいんだけど...という人がいたら、テキストビューに表示する文字を、各画面で決定できるようにするの部分を参考にして欲しいです。完全に同じではないですが、原理は同じです。 コードは以下です ToolBarCustomView.kt interface ToolBarCustomViewDelegate { fun onClickedLeftButton() fun onClickedRightButton() } class ToolBarCustomView : LinearLayout { var delegate: ToolBarCustomViewDelegate? = null constructor(context: Context) : super(context) { init(null, 0) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs, 0) } constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( context, attrs, defStyle ) { init(attrs, defStyle) } private fun init(attrs: AttributeSet?, defStyle: Int) { LayoutInflater.from(context).inflate(R.layout.custom_tool_bar, this, true) } // ツールバーに表示する文字や、ボタンの表示/非表示の切り替えを設定する fun configure(titleText: String, isHideLeftButton: Boolean, isHideRightButton: Boolean) { // カスタムツールバーのImageButtonとTextViewを取得する val titleTextView: TextView = findViewById(R.id.text_title) val leftButton: ImageButton = findViewById(R.id.btn_left) val rightButton: ImageButton = findViewById(R.id.btn_right) // TextViewに文字を設定 // ImageViewの表示/非表示を切り替える titleTextView.text = titleText leftButton.visibility = if (isHideLeftButton) View.INVISIBLE else View.VISIBLE rightButton.visibility = if (isHideRightButton) View.INVISIBLE else View.VISIBLE // ボタンがクリックされたときのリスナーを設定 // 実際の処理は画面ごとのActivityで設定 leftButton.setOnClickListener { delegate?.onClickedLeftButton() } rightButton.setOnClickListener { delegate?.onClickedRightButton() } } } 上記のコードの3つのconstructor()の部分はLinearLayoutを継承したら書かないといけないお決まりの呪文なので心を無にしてコピペです。 それでは、お次はツールバーを表示したい画面のActivityをいじっていきます。 ツールバーを表示したい画面のActivity この画面ではカスタムツールバーにどんな文字を表示させたいか、また、イメージボタンクリック時にどんな処理をさせたいのかを決定(設定)します。 コードはこんな感じです InputDataActivity.kt class InputDataActivity : AppCompatActivity(), ToolBarCustomViewDelegate { // viewModelの初期化 private val viewModel by viewModels<ConfirmViewModel>() // bindingクラスをlateinit varで宣言 private lateinit var binding: ActivityInputDataBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // bindingの初期化とsetContentViewを行う binding = ActivityInputDataBinding.inflate(layoutInflater) .apply { setContentView(this.root) } // デフォルトのアクションバーを非表示にする supportActionBar?.hide() // カスタムツールバーを設置 setCustomToolBar() ... } ... // ===== setCustomToolBar()を実装 ===== private fun setCustomToolBar() { val toolBarCustomView = ToolBarCustomView(this) toolBarCustomView.delegate = this val title = getString(R.string.title_tool_bar) toolBarCustomView.configure(title, false, false) // カスタムツールバーを挿入するコンテナ(入れ物)を指定 val layout: LinearLayout = binding.containerForToolbar // ツールバーの表示をコンテナに合わせる toolBarCustomView.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) // カスタムツールバーを表示する layout.addView(toolBarCustomView) } override fun onClickedLeftButton() { // 前の画面に戻る finish() } override fun onClickedRightButton() { // TODO: メニューを表示 // TODO: メニュークリックでConfirmActivityに画面遷移 } } 上記のコードでは、カスタムツールバーと関係のないところは省略しています。ポイントは、ToolBarCustomViewDelegateというインタフェースを継承するのと、setCustomToolBar()でカスタムツールバーの設定(イメージボタンの表示/非表示の決定やタイトルに表示する文字など)を行うことです。また、盲点なのが、デフォルトのツールバーを非表示にすることです。 この部分ですね // デフォルトのアクションバーを非表示にする supportActionBar?.hide() このコードを書くとその画面からデフォルトのツールバー(アクションバー)が非表示にされます。これをしないとデフォルトのツールバーとカスタムツールバーが二つ出てきてしまいます。 ビルドしてみよう! ここまでできればカスタムツールバーが表示されるはずです!実際にどのように表示されるのか、完成をみてみましょう!こんな感じです!! いい感じです!! ここで皆さんの中にはある「違和感」を覚える人がいるかもしれません。私の画像ではなく、私の記事を読みながらカスタムツールバーを作成したあなたのエミュレータの画面にです。ステータスバーの色、浮いてません?ステータスバーとはwifiとか充電とかが表示されている部分です。そうなんです。ツールバーとステータスバーは別物!だからツールバーだけ色を変えるとステータスバーの色が目立っちゃう!なんか浮いちゃう!なので、ステータスバーの色も好きないろに変更しましょう!themes.xmlのcolorPrimaryVariantっていうところの色を変えるとステータスバーの色が変わります。私の画像ではもう変えてあるので違和感はないかと思います。他にもステータスバーの色の変え方は簡単な方法から難しい方法まで多岐に渡るので調べてみてください。 それでは!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]思考停止でセットしていたLayoutInflaterを少し掘り下げてみる

質問箱にて このような質問を頂いたので、書いている次第でございます。 LayoutInflaterは、RecyclerviewのonCreateViewHolderとか、FragmentのonCreateViewメソッドのパラメータとしてよく使われますね。 正直、全く理解していません。 何なのでしょうか? 掘り下げてみましょう。 簡単に説明すると LayoutInflaterは、「膨らます奴」— Reo@Android Developer (@RunningReo) June 8, 2021 英語の意味として、inflateが「膨らませる」という意味で、なんとからーは○○をする人という意味なので、inflaterは「膨らます奴」ですね。 そして、LayoutInflaterは、レイアウトを「膨らます奴」です。 これだけで、考えるという観点ではLayoutInflaterを扱えるので、思考停止は免れたかと思います。 もう少し掘り下げてみる よくFragmentにてViewを生成する時に、LayoutInflaterクラスのinflateメソッドを用います。 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); return inflater.inflate(R.layout.hogehoge, container, false); } そもそも、FragmentとはActivityを容器としてその中に部分的な画面として表示します。 ここからActivityという容器の中で、膨らませるようなイメージが出来るかなと思います。 LayoutInflaterという「膨らます奴」が、Activityという「容器」の中で、inflateという「膨らます」行為をしているわけですね。 もう少し、理論的にいうとXMLファイルを直接インスタンスせずに利用するイメージです。 第一引数は、「膨らます」XMLファイルを指定します。 第二引数は、ViewGroupでネストの一番外側のビューを表します。 第三引数は、第一パラメータで指定したレイアウトをルートビューにするか否かを ちなみに、質問者さんのonCreateViewHolderもRecyclerViewという「容器」の中で一行分のレイアウトを「膨らませて」作っています。 この膨らませた一行分のレイアウトを使い回しているわけですね。 終わり 質問者さんに満足していただいた記事は書けたのかな・・・ もっと深掘った方が良かったのかな・・・・ 取り敢えず、今の自分が掘れる範囲はここまででした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】SpanでTextの一部装飾

こんにちは、Android開発してるめっしーです。 Twitterはこちらです 始めに Android(kotlin)で文字列を装飾する際に利用するスパンについて紹介します。 下の方にktx使った簡単なものも書きました。 スパンとは スパンは強力なマークアップオブジェクトで、文字単位や段落単位でテキストのスタイルを設定できます。 スパンをテキストオブジェクトにアタッチすると、色、テキストのクリック可視化、文字サイズの拡大縮小、など様々な方法でテキストを変更できます。 種類 スパンを作成するには、以下のリスト表示されているクラスを用途に応じて利用する。 クラス テキストは変更可 マークアップは変更可 データ構造 SpannedString × × リニア配列 SpannableString × ○ リニア配列 SpannableStringBuilder ○ ○ 区間ツリー SpannedString テキストやマークアップを作成後に変更しない場合に利用 SpannableString 単一のテキストオブジェクトに少数のスパンをアタッチした、テキスト自体は読み取り専用にする場合に利用。 SpannableStringBuilder 作成後にテキストを変更する必要があり、テキストにスパンをアタッチする必要がある場合 テキストオブジェクトに多数のスパンをアタッチする必要がある場合は、テキスト自体を読み取り専用にするかどうかにかかわらず利用 実装 基本形 スパン適用するには setSpan(Object what, int start, int end, int flags)を呼び出す val spannable = SpannableStringBuilder("Sample Text") spannable.setSpan( ForegroundColorSpan(Color.RED), 6, // start 10, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) flag 説明 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 挿入テキストを含める場合 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 挿入テキストを除外する場合 文字のRangeをとる拡張関数 Spanを使う時に文字の一部を装飾したい機会が多いと思われます。 その際に特定の文字のRangeを取得できるものがあると便利なので以下のような拡張関数を作ります。 この後のサンプルもこれを使ったもので説明していきます。 fun String.rangeOfIndex(string: String): IntRange { val startIndex = indexOf(string) return startIndex until startIndex + string.length } KTX ktxの拡張機能にもスパンがあります 依存関係 dependencies { implementation("androidx.core:core-ktx:1.5.0") } スパンでできるスタイル スパンで可能となってる装飾を色々紹介していきます。 ktxで出来るものはその実装も一緒に載せておきます。 色 ForegroundColorSpan(Color.RED)を利用 private fun spanColor(): SpannableStringBuilder { val sampleLabel = "Sample Text" val colorRange = sampleLabel.rangeOfIndex("Text") return SpannableStringBuilder(sampleLabel).apply { setSpan( ForegroundColorSpan(Color.RED), colorRange.first, // start colorRange.last.inc(), // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } //kts private fun spanColorKtx(): SpannedString { val sampleLabel = "Sample Text" val colorRange = sampleLabel.rangeOfIndex("Text") return buildSpannedString { append(sampleLabel.subSequence(0, colorRange.first)) color(Color.RED) { append(sampleLabel.subSequence(colorRange)) } } } アンダーライン UnderlineSpan()を利用 private fun spanUnderline(): SpannableStringBuilder { val sampleLabel = "Sample Text" val underlineRange = sampleLabel.rangeOfIndex("Text") return SpannableStringBuilder(sampleLabel).apply { setSpan( UnderlineSpan(), underlineRange.first, // start underlineRange.last.inc(), // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } //ktx private fun spanUnderlineKtx(): SpannedString { val sampleLabel = "Sample Text" val underlineRange = sampleLabel.rangeOfIndex("Text") return buildSpannedString { append(sampleLabel.subSequence(0, underlineRange.first)) underline { append(sampleLabel.subSequence(underlineRange)) } } } 文字サイズ RelativeSizeSpan(1.5f)を利用 50%大きくする場合に private fun spanSize(): SpannableStringBuilder { return SpannableStringBuilder("Sample Text").apply { setSpan( RelativeSizeSpan(1.5f), 7, // start this.length, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } //kts private fun spanSizeKtx(): SpannedString { val sampleLabel = "Sample Text" val underlineRange = sampleLabel.rangeOfIndex("Text") return buildSpannedString { append(sampleLabel.subSequence(0, underlineRange.first)) scale(1.5f) { append(sampleLabel.subSequence(underlineRange)) } } } 背景に色を BackgroundColorSpan()を利用 private fun spanBackgroundColor(): SpannableStringBuilder { return SpannableStringBuilder("Sample Text").apply { setSpan( BackgroundColorSpan(Color.RED), 7, // start this.length, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } //ktx private fun spanBackgroundKtx(): SpannedString { val sampleLabel = "Sample Text" val underlineRange = sampleLabel.rangeOfIndex("Text") return buildSpannedString { append(sampleLabel.subSequence(0, underlineRange.first)) backgroundColor(Color.RED) { append(sampleLabel.subSequence(underlineRange)) } } } 段落 QuoteSpan()を利用 private fun spanParagraph(): SpannableStringBuilder { return SpannableStringBuilder("Sample \nText").apply { setSpan( QuoteSpan(), 0, // start this.length, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } API28からは段落の横のスタイルを変えることもできます。 @RequiresApi(Build.VERSION_CODES.P) private fun spanParagraph(): SpannableStringBuilder { return SpannableStringBuilder("Sample \nText").apply { setSpan( QuoteSpan(Color.GREEN, 20, 40), 0, // start this.length, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } クリック処理 private fun spanClick(): SpannableStringBuilder { val sampleLabel = "Sample Text" val sizeRange = sampleLabel.rangeOfIndex("Text") return SpannableStringBuilder(sampleLabel).apply { setSpan( object : ClickableSpan() { override fun onClick(widget: View) { // TODO クリック処理 } }, sizeRange.first, // start sizeRange.last.inc(), // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } //ktx private fun spanClickKtx(): SpannedString { val sampleLabel = "Sample Text" val sizeRange = sampleLabel.rangeOfIndex("Text") return buildSpannedString { append(sampleLabel.subSequence(0, sizeRange.first)) inSpans( object : ClickableSpan() { override fun onClick(widget: View) { // TODO クリック処理 } }, ) { append(sampleLabel.subSequence(sizeRange)) } } } クリック処理を行うには以下も忘れずに termsText.apply { text = spanClick() //追加 movementMethod = LinkMovementMethod.getInstance() } movementMethod TextViewにナビゲーションの目的で、キーイベント、トラックボール、モーション、およびタッチの処理をmoveメソッドに委任します。 LinkMovementMethod テキスト内のリンクを移動し、必要に応じてスクロールするためのクラス。 複数のスパン 1つのテキストに複数のスパンをアタッチできる 例)色とsyleを指定する private fun spanMultiple(): SpannableString { val sampleLabel = "Sample Text" val boldRange = sampleLabel.rangeOfIndex("Text") val colorRange = sampleLabel.rangeOfIndex("Te") return SpannableString(sampleLabel).apply { setSpan( ForegroundColorSpan(Color.RED), colorRange.first, // start colorRange.last.inc(), // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) setSpan( StyleSpan(Typeface.BOLD), boldRange.first, boldRange.last.inc(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) } } サンプルレポジトリ こちらは自分が行ってきた技術サンプルが詰まってます。 その中で今回は SpanFragment.ktに今回のコードが書かれてあります。ぜひご覧ください。 参考 ではまた!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidStudioのファイル構成

勉強していて、AndroidStudioのフォルダやファイルが何を意味するのかを理解する為にまとめていきます。 androidビューのファイル構成 主に使用するファイルはmanifesuts,java,resの3フォルダに分かれます。 manifesuts このフォルダには、AndroidManifest.xmlファイルが格納されています。AndroidManifest.xmlは、このアプリの実行に必要な設定が記述されています。 java このフォルダの中には、.javaファイルと.ktファイルを格納する。なお、パッケージ右側に「(androidTest)」や、「(Test)」と記述されているのは、Androidアプリをテストするための。javaファイル、.ktファイルの格納先です。 res Androidでは、画面構成をXMLファイルに記述する。そういった.xmlファイルや、アプリで使われる画像ファイルなどを格納するのが、resフォルダである。ちなみにresはresorceの略です。 resフォルダ内のサブフォルダ構成 サブフォルダ名 内容 drawable 画像を格納 layout 画面構成に関わる.xmlファイルを格納 mipmap アプリのアイコンを格納 values アプリで表示する固定文字列(string.xml)、画面のスタイル(styles.xml)、色構成(colors.xml)を表す.xmlファイルを格納 補足  Gradel Scriptsノード Android StudioではビルドシステムとしてGladleを使用していますが、そのビルドスクリプトは目的ごとにファイルが分かれ、配置ディレクトリも分散されています。それをまとめて表示してくれるのが、このGladel Scriptノードです。 実際に編集する主なファイル 通常のアプリ開発でよく編集するファイルは以下の3つ [1]res/layoutフォルダ中のレイアウトXMLファイル [2]javaフォルダ中の.ktファイル [3]res/valuesフォルダ中のstrings.xmlファイル 画面構成を.xmlファイルに、処理をKotlinクラスに記述する為、.xmlファイルとKotlinクラスのペアで1つの画面が作られる。 画面構成用の.xmlファイルをレイアウトファイルと呼び、 Kotlinクラスのことをアクティビティを呼ぶ。 つまり、[1],[2]はこのペアを表している。 それ故に、プロジェクトを進めていく上で、[1][2]のファイル名を関連した名前を設定すると効率が上がる。 [3]は、アプリ中で使われる画面に表示させる文字列は、[1]のファイルや,[2]のソース中に直接入力するのではなく、[3]のstrings.xmlに記述する。 アプリ開発の手順 1,プロジェクトを作成する。 2,strings.xmlに表示文字列を記述する。 3,レイアウトxmlファイルに画面構成を記述する。 4,アクティビティなどの.ktファイルに処理を記述する。 5,アプリを起動して動作確認をする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]Retrofitのレスポンスのハンドリングを楽にするかもしれない2つのライブラリ

はじめに Android 開発において Retrofit はよく使われるライブラリの一つかと思います。 今回は Retrofit のレスポンスのハンドリングを楽にしてくれるかもしれないライブラリを2つご紹介したいと思います。 slackhq/EitherNet skydoves/Sandwich 2つのライブラリの特徴 この2つのライブラリの共通の特徴として、ライブラリが定義している独自のクラスでレスポンスを受け取れます。 EitherNet の方は ApiResult、Sandwich の方は ApiResponse という独自のクラスが定義されており、これらのクラスは sealed class になっています。 これらの sealed class のサブクラスが通信の成功や失敗を表す型になっているため、クラスの型を判別すれば通信処理の結果が判定できます。 Retrofit のエラーハンドリングは手間がかかる? 例えば Qiita の API で記事一覧を取得することを考えます。 まずは以下のように Retrofit で使うためのインターフェースを定義します。 interface QiitaApi { @GET("/api/v2/items") suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): List<Item> } この場合、記事一覧の取得は以下のように呼び出します。 suspend fun fetchItems() { val items = qiitaApi.findItems(1, 30) // items を使った処理... } ただ、この場合通信処理を行うので、もしかしたらネットワーク状態が悪くて通信が失敗してしまうこともあり得ます。 その場合、以下のようにエラーハンドリングを行う必要があります。 suspend fun fetchItems() { try { val items = qiitaApi.findItems(1, 30) // items を使った処理... } catch (e: Throwable) { // エラー処理... } } また API からレスポンスは返ってきたけど、HTTP ステータスコードが400以上の場合にはレスポンスの内容をパースして別に処理を行いたい、などの場合も考えられます。 その場合は Retrofit の HttpException の例外を捕捉して処理する必要があります。 suspend fun fetchItems() { try { val items = qiitaApi.fetchItems(1, 30) // items を使った処理... } catch (e: HttpException) { if (e.code() == 400) { val errorBody = e.response()?.errorBody() // HTTP ステータスコードが400の場合のエラー処理... } } catch (e: Throwable) { // エラー処理... } } このように Retrofit で API 呼び出し時に自前でエラーハンドリングをするのは結構手間がかかってしまうかもしれません。 そこで先ほど挙げた2つのライブラリを使って、レスポンス時の処理を変更してみようと思います。 slackhq/EitherNet EitherNet の方では ApiResult<out T, out E> でレスポンスの結果を受け取ることができ、v0.2.0 時点での定義は以下のようになっています。 public sealed class ApiResult<out T, out E> { public data class Success<T : Any>(public val response: T) : ApiResult<T, Nothing>() public sealed class Failure<out E> : ApiResult<Nothing, E>() { // 通信エラー. public data class NetworkFailure internal constructor(public val error: IOException) : Failure<Nothing>() // 予期しないエラー(レスポンスの JSON のパースエラーなど). public data class UnknownFailure internal constructor(public val error: Throwable) : Failure<Nothing>() // HTTP ステータスコードが 4xx or 5xx の時のエラー. public data class HttpFailure<out E> internal constructor(public val code: Int, public val error: E?) : Failure<E>() // レスポンスボディを自前で変換する時に ApiException を throw した際のエラー. public data class ApiFailure<out E> internal constructor(public val error: E?) : Failure<E>() } } T は成功時の型、E はエラー時の型になります。 ApiResult には2つのサブクラス Success と Failure があり、それぞれ通信の成功と失敗を表す型になっています。 Failure にはさらに4つのサブクラスが定義されていて、エラーの発生要因によってそれぞれ対応するクラスのインスタンスがレスポンスとして返るようになります。 使い方 例として、先ほどの Qiita の API に対して対応してみます。 まずは通信成功、失敗時のレスポンスのデータクラスを作ります。 レスポンスの JSON 文字列をパースするために Moshi を使います。 // Qiita の記事の投稿データ. // https://qiita.com/api/v2/docs#投稿 @JsonClass(generateAdapter = true) data class Item( @Json(name = "id") val id: String, @Json(name = "title") val title: String ) // Qiita の API のエラーレスポンス // https://qiita.com/api/v2/docs#エラーレスポンス @JsonClass(generateAdapter = true) data class QiitaApiError( @Json(name = "message") val message: String, @Json(name = "type") val type: String ) Qiita API のエラーレスポンスは message と type というキーを含んだ JSON オブジェクトの文字列が返ってくるので、上記のように定義することができます。 interface QiitaApi { @DecodeErrorBody // HttpFailure 時にレスポンスボディを QiitaApiError に変換するために必要 @GET("/api/v2/items") suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): ApiResult<List<Item>, QiitaApiError> } val qiitaApi = Retrofit.Builder() .addConverterFactory(ApiResultConverterFactory) .addCallAdapterFactory(ApiResultCallAdapterFactory) .addConverterFactory(MoshiConverterFactory.create()) // addConverterFactory(ApiResultConverterFactory) より後に設定する必要がある .build() .create<QiitaApi>() Retrofit のインターフェースの定義と作成は上記のように行います。 ApiResult<List<Item>, QiitaApiError> は通信成功時には List<Item>、通信失敗(HttpFailure or ApiFailure)時には QiitaApiError でレスポンスを受け取ることができます。 @DecodeErrorBody アノテーションを付けないとエラー時のレスポンスボディを QiitaApiError に変換してくれないので注意が必要です。 もう一つ注意点としては、Retrofit のインスタンスを作成する際の addConverterFactory の呼び出しの順序です。 addConverterFactory(ApiResultConverterFactory) より後に addConverterFactory(MoshiConverterFactory.create()) を呼び出さないと正しくレスポンスがパースされないので注意してください。 そして以下のように呼び出して、結果をハンドリングすることができます。 when (val result = qiitaApi.fetchItems(1, 3)) { is ApiResult.Success -> Log.d(TAG, "Success: ${result.response}") is ApiResult.Failure -> when (result) { // result.error は QiitaApiError 型のインスタンス is ApiResult.Failure.HttpFailure -> Log.e(TAG, "HttpFailure: ${result.code}, ${result.error}") is ApiResult.Failure.ApiFailure -> Log.e(TAG, "ApiFailure: ${result.error}") is ApiResult.Failure.NetworkFailure -> Log.e(TAG, "NetworkFailure: ${result.error}") is ApiResult.Failure.UnknownFailure -> Log.e(TAG, "UnknownFailure: ${result.error}") } } skydoves/Sandwich Sandwich の方では ApiResponse<out T> で結果を受け取ることができ、v1.1.0 時点での定義は以下のようになっています。 sealed class ApiResponse<out T> { data class Success<T>(val response: Response<T>) : ApiResponse<T>() { val statusCode: StatusCode = getStatusCodeFromResponse(response) val headers: Headers = response.headers() val raw: okhttp3.Response = response.raw() val data: T? = response.body() override fun toString() = "[ApiResponse.Success](data=$data)" } sealed class Failure<T> { // HTTP ステータスコードが200番台以外のエラー(ただし設定で変更は可能). data class Error<T>(val response: Response<T>) : ApiResponse<T>() { val statusCode: StatusCode = getStatusCodeFromResponse(response) val headers: Headers = response.headers() val raw: okhttp3.Response = response.raw() val errorBody: ResponseBody? = response.errorBody() override fun toString(): String = "[ApiResponse.Failure.Error-$statusCode](errorResponse=$response)" } // Failure.Error 以外のエラー. data class Exception<T>(val exception: Throwable) : ApiResponse<T>() { val message: String? = exception.localizedMessage override fun toString(): String = "[ApiResponse.Failure.Exception](message=$message)" } } } T は成功時の型になります。 EitherNet と同じように ApiResponse にも2つのサブクラス Success と Failure があり、それぞれ通信の成功と失敗を表す型になっています。 Failure にはさらに2つのサブクラス Error と Exception が定義されています。 Failure.Error は HTTP ステータスコードが200番台以外の場合のエラー(ただし設定で変更することが可能)で、それ以外のエラーの場合は Failure.Exception になります。 使い方 Sandwich の README を見るとわかりますが、このライブラリは多くの機能が備わっています。 今回は EitherNet と同じように suspend 関数で API 呼び出しをする際の使い方について、簡単に紹介します。 interface QiitaApi { @GET("/api/v2/items") suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): ApiResponse<List<Item>> } val qiitaApi = Retrofit.Builder() .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory()) .addConverterFactory(MoshiConverterFactory.create()) .build() .create<QiitaApi>() Retrofit のインターフェースの定義と作成は上記のように行います。 ApiResponse 型の戻り値の suspend 関数を呼び出すには addCallAdapterFactory(CoroutinesResponseCallAdapterFactory()) を設定する必要があります。 そして以下のように呼び出して、結果をハンドリングすることができます。 when (val result = qiitaApiForSandwich.fetchItems(1, 3)) { is ApiResponse.Success -> Log.e(TAG, "Success: ${result.response.body()}") is ApiResponse.Failure.Error -> Log.e(TAG, "Error: ${result.statusCode}, ${result.errorBody?.string()}") is ApiResponse.Failure.Exception -> Log.e(TAG, "Exception: ${result.exception}") } エラー時のレスポンスボディをパースして QiitaApiResult に変換したい場合などは、Mapper などの機能を使って変換する必要があります。 Failure.Error 時に Mapper を適用して QiitaApiResult? に変換する例は以下になります。 class QiitaApiErrorMapper(private val moshi: Moshi) : ApiErrorModelMapper<QiitaApiError?> { override fun map(apiErrorResponse: ApiResponse.Failure.Error<*>): QiitaApiError? { return apiErrorResponse.errorBody?.source()?.let(moshi.adapter(QiitaApiError::class.java)::fromJson) } } when (val result = qiitaApiForSandwich.fetchItems(1, 3)) { is ApiResponse.Failure.Error -> result.onError(QiitaApiError.Mapper(moshi)) { Log.e(TAG, "Error: ${this?.message}") } ... } 2つのライブラリの比較 私が感じた EitherNet と Sandwich との違いについてです。 使いやすさ EitherNet の方がシンプルに思いました。 Sandwich は多機能なため、機能を把握するのに少し大変かもしれません。 エラーハンドリングについて EitherNet はエラー時のレスポンスの型も設定できるので、Moshi と併用すればエラーレスポンスの変換が簡単に行えました。 Sandwich の方は Mapper を作成してエラーレスポンスを変換する必要があるので、少しだけ手間が増える感じです。 レスポンスヘッダへのアクセス レスポンスヘッダの中身を扱いたい場合は Sandwich 一択になります。 Sandwich では ApiResponse.Success と ApiResponse.Failure.Error のインスタンスでレスポンスヘッダを扱うことができますが、EitherNet ではできません。 メンテナンス状況 2021年6月現在、EitherNet は最近あまりメンテナンスされていないようです。 Sandwich は定期的に機能追加などのメンテナンスが行われているようです。 最後に 簡単にですが EitherNet と Sandwich について紹介させていただきました。 Retrofit のレスポンスのハンドリングが楽になるかもしれないので、興味のある方は使ってみてはいかがでしょうか。 https://github.com/watabee/RetrofitResponsesSample にサンプルプロジェクトを公開しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidStudio 静的メニューの定義

参考書「作って楽しむプログラミング Androidアプリ超入門」の勉強をまとめていきます。 メニューを定義する android端末でメニューボタンをタッチすると、設定や、保存などのオプションメニューが表示される。 メニューには、あらかじめ決められた項目を表示する静的メニューと、アプリの状態に応じて変更する動的メニューがある。 今回は静的メニューを作成する メニューの定義はXMLファイルを作成する ->AndroidStudioでresフォルダを右クリックし、New > Android Resource File menu.xml <?xml version="1.0" encoding="utf-8"?> <menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android">         //item1つに1のメニュー内容を記載          //android:id ;選択肢のid         //android:title ; 表示文字列 <item android:id="@+id/item1" android:title="設定"></item> <item android:id="@+id/item2" android:title="このアプリについて"></item> </menu> item属性の中には、app:showAsAction : アクションバーに表示するかどうかもある。 メニューを表示する オプションメニューを表示するには、画面のタッチやセンサーを利用する場合と同様にイベントに応じて呼び出されるコードを追加する。 しかし、メニューの場合は、新たにイベントリスナーを設定する必要はなく、Activityクラスのメソッド(あらかじめ呼び出されるメソッドが定義されている)をオーバーライドするだけで表示ができる。 ->AndroidStudioのmainActivity.kt > Codeメニュー > Overrride Menthods >イベント毎のメソッドを選択 主なメニューのイベント メニューのイベント 実行されるメソッド タイミング メニューが作成される時 onCreateOptionMenu 起動時のみ メニュー表示する時 onPrepareOptionMenu 毎回 メニューの項目を選択した時 onOptionsItemSelected 選択時のみ メニューが閉じられる時 onOptionMenuClosed 毎回 参考 https://qiita.com/watataku8911/items/5faad0384b54d0c53f6e https://developer.android.com/guide/topics/resources/menu-resource?hl=ja
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin研修5日目】フラグメントのライフサイクルおよび実装

フラグメント 参考: フラグメント マルチデバイス対応などで用いられる、画面サイズに応じてレイアウトを変えられる仕組み。 端末の1画面を複数の区画に分割することで、各区画に複数のフラグメント画面を埋め込めるようにしている。 なお、フラグメント画面(=Fragmentファイル)はアクティビティと同様、レイアウトファイルとアクティビティで構成される。 フラグメントのライフサイクル フラグメントは、まずフラグメントを埋め込むアクティビティと関連付けられ、 その後にフラグメントがもつビューと関連付けられる。 フラグメント⇄アクティビティのライフサイクル関係 出典 フラグメント⇄ビューのライフサイクル関係 出典 ビュー⇄フラグメント⇄アクティビティのライフサイクル フラグメント ビュー Fragmentメソッド 呼び出しタイミング Created onAttach() アクティビティとの最初の関連付け Created onCreate() フラグメントの初期化 Created Initialized onCreateView() ビューレイアウトのインフレート(=ビューとの関連付け) Created Initialized onViewCreated() onCreateView()の終了 Created Created onActivityCreated() アクティビティの生成完了、ビューレイアウトの実体化完了(=Activity.onCreate()) Created Created onViewStateRestored() 保存済みビューUIの復元完了 Started Started onStart() フラグメントの表示(=Activity.onStart()) Resumed Resumed onResume() フラグメントの操作受付完了(=Activity.onResume()) Started Started onPause() フラグメントの一時停止(=Activity.onPause()) Created Created onStop() フラグメントの停止(=Activity.onStop()) Created Created onSaveInstanceState() ビューのUI状態の保存 Created Destroyed onDestroyView() ビューとの関連付け解除 Destroyed onDestroy() フラグメントの利用停止 Destroyed onDetach() アクティビティとの関連付け解除 フラグメントによる画面の実装 フラグメントを実装するには、Fragmentファイル(=レイアウトファイルとアクティビティの組み合わせ)を作成する。 レイアウトのインフレート(フラグメントの生成) 定義 LayoutInflater.inflate( resource: Int, root: ViewGroup?, attachToRoot: Boolean ): View! // パラメータ // resource: レイアウトファイル名(R値) // root: インフレートしたビューを配置する親ビュー // -> 通常はonCreateView()の仮引数containerを記述 // attachToRoot: インフレートしたビューをroot配下に配置するかどうか // -> 通常はfalseを記述 サンプルコード MenuListFragment.kt class MenuListFragment: Fragment() { // フラグメントのレイアウト(XML)のインフレート(フラグメントの生成) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // インフレートしたビュー val view = inflater.inflate( R.layout.<フラグメントのレイアウトファイル名>, container, false ) ... return view } } アクティビティへの埋め込み fragmentタグを用いてレイアウトファイル(=activity_main.xml)に記述する。 埋め込むフラグメントは、android:name属性に完全修飾名(com.~.<フラグメントクラス(=ファイル)名>)を記述して指定する。 サンプルコード activity_main.xml <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:id="@+id/fragmentMenuList" android:name="com.example.fragment.MenuListFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout> FrameLayout 参考: 研修1日目 z方向にビューを重ねて配置できるレイアウト部品。 フラグメントでのオプションメニュー実装 参考: 研修4日目 FragmentクラスでのonCreateOptionMenu()、onOptionsItemSelected()メソッドの実装に加え、 onCreate()メソッドのブロック{...}内にsetHasOptionsMenu(true)メソッドを記述する。 MenuListFragment.kt // 発展: class MenuListFragment: Fragment() { override fun onCreate(savedInstanceState: Bundle?) { ... setHasOptionsMenu(true) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) ... } override fun onOptionsItemSelected(item: MenuItem): Boolean { ... return super.onOptionsItemSelected(item) } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidstudioでのgit連携

Githubトークン作成方法 Githubにログイン -> Settings -> Developer settings -> Personal access tokens -> Generate new token -> Note: 名前、  -> Select scope↓ admin:repo_hook にもチェック! -> VCS -> Import into Version Control -> share project on github -> token入力してログイン
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

flutter で行こう;iOS,Android 自由自在

flutterという言葉をチラホラ耳にする機会も多くなったと思う、またコーダーであればiOS,Androidのapp開発でそれぞれXcodeとAndroid Studioの環境を使い分ける事にストレスを感じる事も多いと思う。 これらの環境を一元的にコーダーの視点で統一的に開発ができる様になった環境がflutterだ。 是非この機会に接する事をオススメする。とは言え私もflutter熟練者ではないので、皆さんと同じ様にTry&Errorをしつつ参考を示していきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む