- 投稿日:2020-02-21T18:56:58+09:00
【Android】Fragment切り替えの時にアプリバーを更新する
概要
FragmentTransactionを
replace()
からadd()
に変更した際、Fragment遷移時に更新していたアプリバーのタイトルやメニューボタン等が更新されなくなった。
add()
の場合でもアプリバーを更新できる方法をまとめる。この記事ではonBackStackChangedListener
を使っている。Fragment遷移時にアプリバーの要素を更新する
MainActivity.ktclass MainActivity : AppCompatActivity() { val activeFragment: BaseFragment? get() { val fragment = supportFragmentManager.findFragmentByTag("baseFragment") return fragment as? BaseFragment } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... (略) ... // BackStackが変更された時 supportFragmentManager.addOnBackStackChangedListener { activeFragment?.updateActionBar() } } }BaseFragment.ktabstract class BaseFragment : Fragment() { ... (略) ... override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { updateActionBar() return null } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) } open fun updateActionBar() { appActivity?.supportActionBar?.apply { setTitle("") // 戻るボタンを表示するかどうか setDisplayHomeAsUpEnabled(showsBackBarButton) setBackgroundDrawable(ColorDrawable(Color.parseColor("#008577"))) } } }BackStackが変更された時に、Fragment遷移後にアプリバーを更新する(
activeFragment
は遷移後のFragmentになる)。
各FragmentはBaseFragment
を継承して作成し、updateActionBar()
をオーバーライドして、Fragmentごとのタイトルやメニュー等をセットする。HomeFragment.ktclass HomeFragment : BaseFragment() { ... (略) ... override fun updateActionBar() { super.updateActionBar() this.setHasOptionsMenu(true) appActivity?.supportActionBar?.title = "ホーム" } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.home_menu, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_next -> { // SampleFragmentへ遷移するコード true } else -> { super.onOptionsItemSelected(item) } } } }SampleFragment.ktclass SampleFragment : BaseFragment() { ... (略) ... override fun updateActionBar() { super.updateActionBar() this.setHasOptionsMenu(true) appActivity?.supportActionBar?.apply { setTitle("サンプル") setBackgroundDrawable(ColorDrawable(Color.RED)) } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.sample_menu, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_sample -> { // icon pressed true } else -> { super.onOptionsItemSelected(item) } } } }
これで、Fragment遷移の時にアプリバーのタイトルやメニューなどの要素が更新されるようになっ……あれ?しかもこの「次へ」ボタンも押せるため、SampleFragmentとそのメニューアイコンが無限に増える。無限ループって怖くね?
表示しているFragmentのメニューのみを表示する
SampleFragmentに遷移したにもかかわらず、HomeFragmentのメニューが表示されたままになってしまっている。
そこで、現在表示しているFragmentのメニューのみを表示したい。画面遷移をする時に、
this.setHasOptionsMenu(false)
で遷移前のFragmentのメニューを隠すことにする。HomeFragment.ktclass HomeFragment : BaseFragment() { ... (略) ... override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_next -> { // このFragmentのメニューを隠す this.setHasOptionsMenu(false) // SampleFragmentへ遷移するコード true } else -> { super.onOptionsItemSelected(item) } } } }まとめ
onBackStackChangedListener
を使って、Fragment遷移時にアプリバーの要素を更新した。this.setHasOptionsMenu(false)
でFragment遷移時に遷移前のFragmentのメニューを隠した。
- 投稿日:2020-02-21T14:55:17+09:00
iOS/androidアプリのレイアウト微調整がめんどくさかった話
はじめに
iOSアプリのレイアウト微調整、めんどくさいですね。
機種もたくさんあるし、スクショ撮ってくらべてやり直して、また撮って・・・。
おまけにモックとスクショのサイズをあわせてくらべたりね。超面倒ってやってたんですが、もっと楽な方法がありました。
楽な方法
シミュレータ/エミュレータと、できればzeplin等を使う。
zeplinという便利ツールでデザイナーとやりとりしてて、モックもそれなんですが、サイズがiOSシミュレータと一緒なので、リサイズも必要無し。スクショ無し。
並べて比べるのみ。
Androidはzeplinでzoom to fitして同じサイズにして使う。zeplinじゃなくてもいいしね。モックの表示サイズ調整できれば良し。
終わりに
一時期ライブラリの関係でシミュレータ向けビルドができなくなってて、すっかり使わなくなってたんですが、シミュレータをガシガシ使った方が楽だってことに気がつきました。
日英切り替えとかも2台起動しておいて、それぞれにしておいて、設定切り替え無しで確認できますしね。
もっと早く気づけば良かった
超絶に楽になりました。
- 投稿日:2020-02-21T14:33:02+09:00
[Android]Dagger2の導入
はじめに
AndroidアプリにDaggerを導入した際の手順と
自分がDaggerを学び始めた際に、とっかかりとして欲しかったなぁと思う情報をまとめます。今回の記事は自分自身がDagger初心者ということもあり、初歩的な内容となっています。
その分、同じDagger初心者の方に分かりやすく書けたらいいなと思いこの記事を書き始めました。Daggerは、JavaやKotlinでDI(Dependency Injection)を行うことをサポートするライブラリです。
DIはたびたび「依存性の注入」と訳されますが、この訳がDIを意味不明なものにしていると思っています。
ネットで検索していると「DI = 依存オブジェクトの注入」と訳されている方がおり、
とてもスッキリしたので本記事でも「DI = 依存オブジェクトの注入」で進めようと思います。
Dependency Injectionを「依存性の注入」と訳すのは非常に悪い誤訳Dependency Injection
例えば、ViewModelでSharedPreferencesを扱う必要があるとき、
SharedPreferencesのラッパークラスをSharedPreferenceManagerとして定義していたとすると、
単純に実装すると下記の例のようになると思います。class SampleViewModel ( app: Application ) : AndroidViewModel(app) { private val prefManager: SharedPreferenceManager = SharedPreferenceManager(app.applicationContext) }このようにすると、
ViewModelが本来知る必要のないSharedPreferenceManagerのインスタンス生成方法を知る必要が出てきます。
ここで、ViewModelで必要なオブジェクト(依存オブジェクト = Dependency)を外部から注入(Injection)するのがDependency Injectionです。class SampleViewModel ( private val prefManager: SharedPreferenceManager ) : ViewModel()ただし、このように単純にコンストラクタで依存オブジェクトを注入しても
結局、呼び出し元(この場合はActivityやFragment)でも知る必要のない
SharedPreferenceManagerのインスタンス生成方法を知る必要が出てきます。
このジレンマを簡単に解消してくれるのがDaggerです。導入
開発言語はKotlinです。
まず、app/build.gradleにDaggerへの依存関係を追加します。
dependencies { def dagger_version = "2.26" // dagger implementation "com.google.dagger:dagger:$dagger_version" kapt "com.google.dagger:dagger-compiler:$dagger_version" }画面上部の"Sync Now"をクリックしてDaggerへの依存関係を解決します。
アノテーション
Daggerでは、変数やクラス、コンストラクタ等にアノテーションをつけることで
クラス同士の依存関係を定義できたり、インスタンスのスコープを明確にしたりすることができます。ここでは、Daggerを導入することで利用できる下記のアノテーションについて
簡単な説明を記載したのちに、具体的な実装例を記載します。
- Component
- Module
- Inject
- BindsInstance
- Provide
- Singleton
Component
Componentアノテーションが付与されたインターフェースは、オブジェクト注入屋さんです。
オブジェクトを注入して欲しいと宣言されている(Injectアノテーションが付与されている)変数やコンストラクタにオブジェクトを注入してくれます。Module
Moduleはインスタンス生成屋さんです。
Componentが見つけた、Injectアノテーションが付与された変数のインスタンスを生成します。
各クラスで知る必要のなかったインスタンスの生成方法は、Moduleが全て知ることになります。Inject
クラスのコンストラクタやフィールドに対してInjectアノテーションを付与することで
Componentがそれを見つけて、クラスが依存するオブジェクトを注入してくれます。BindsInstance
Componentに対してインスタンスをバインドするアノテーションです。
主に、開発者は生成方法を知らない(知る必要がない)けれど、アプリ内で必要なインスタンスがバインドされます。
Androidアプリケーションの場合、Contextをバインドすることがほとんどかなと思っています。Provide
Module内で、インスタンスの生成方法を記載する際に利用するアノテーションです。
Moduleをインスタンス生成屋さんとすると、Provideはレシピのようなものです。Singleton
インスタンスをシングルトンで生成したい時に付与するアノテーションです。
実装例
まずは、オブジェクト注入屋さんであるComponentを定義します。
@Singleton @Component interface AppComponent { @Component.Factory interface Factory { fun create(@BindsInstance context: Context): AppComponent } }Componentには、Component自体を生成するFactoryインターフェースを宣言します。
Factoryインターフェースには、Componentを生成することを示すため
@Component.Factoryアノテーションを付与します。createメソッドの引数として受け取るcontextには、BindsInstanceアノテーションを付与して
contextインスタンスをComponentにバインドします。次に、インスタンス生成屋さんであるModuleを定義します。
@Module class AppModule { @Singleton @Provides fun provideSharedPreferenceManager( context: Context ): SharedPreferenceManager { return SharedPreferenceManager(context) } }ここでは、SharedPreferenceを管理する自作クラス(SharedPreferenceManager)の
インスタンス生成方法をModuleに記載し、
さらにSingletonアノテーションを付与することで、SharedPreferenceManagerをシングルトンインスタンスとして提供するよう定義しています。そしてComponentに戻り、インスタンス生成屋さん(Module)の存在を伝えます。
@Singleton @Component( modules = [AppModule::class] ) interface AppComponent { @Component.Factory interface Factory { fun create(@BindsInstance context: Context): AppComponent } }ここまで進めば、あとはどこに対してDIを適用するかを決めるだけです。
今回はアプリ全体に適用させたいので、Applicationクラスを継承したクラスにComponentを持たせてアプリ全体にDIを適用させます。class MyApplication : Application() { val appComponent: AppComponent by lazy { DaggerAppComponent.factory().create(applicationContext) } }これにより、Componentはアプリ内全域のInjectアノテーションが付与された変数に対して
Moduleで生成されたインスタンスを注入することができるようになります。下記は、SharedPreferenceManagerに依存するSampleViewModelクラスを定義する際の例です。
class SampleViewModel @Inject constructor( private val preferenceManager: SharedPreferenceManager ) : ViewModel()最後に
Daggerの概念が分かりにくく、導入に苦労しましたが、
導入してみるとアプリの全体像がかなり把握しやすくなり、インスタンスの取得が簡単にできるのでコードの可読性も上がりました。今回紹介したのはDaggerのほんの一部の機能で、私自身もまだまだ勉強中です。
誤記等ありましたらコメントでご指摘頂けますと幸いです!参考
- 投稿日:2020-02-21T12:51:20+09:00
Android10でimage_pickerを使ってライブラリから画像を読み込めない時の対処方法
画像が読み込めない!
Flutterで写真にアクセスしようとしてimage_pickerプラグインを使用している
Android10の端末でファイルにアクセスしようとするとpermission denied
targetSdkVersionを29にしていると発生TL;DR
対処方法のみ記載
以下リンクにあるようにAndroidManifestにおまじないを書く<manifest ... > <!-- This attribute is "false" by default on apps targeting Android 10 or higher. --> <application android:requestLegacyExternalStorage="true" ... > ... </application> </manifest>参考リンク:
https://developer.android.com/training/data-storage/compatibility
https://developer.android.com/training/data-storage/shared/media?hl=ja#storage-permissionissue
https://github.com/flutter/flutter/issues/41459実装などはあとで記載します。
- 投稿日:2020-02-21T12:47:30+09:00
Androidのソフトウェアキーボードの高さと表示状態をお手軽取得
概要
Androidのキーボードの高さと表示されているかを知る方法が標準で用意されていないので自作してみました.
そのままコピペすれば動きます.
コードは10行くらいです.ソースコード
var layoutBottom: Int = 0 // 現在のlayoutのbottomを保存 var isShowingKeyboard: Boolean = false // キーボードが表示されていればtrue var keyboardHeight: Int = 0 // キーボードの高さ override fun onCreate(savedInstanceState: Bundle?) { val mRootView = window.decorView.findViewById<ViewGroup>(android.R.id.content) layoutBottom = mRootView.height mRootView.viewTreeObserver.addOnGlobalLayoutListener { val rect = Rect() mRootView.getWindowVisibleDisplayFrame(rect) isShowingKeyboard = layoutBottom > rect.bottom keyboardHeight = abs((layoutBottom - rect.bottom)) layoutBottom = rect.bottom } }
- 投稿日:2020-02-21T09:14:02+09:00
[Android] installreferrer:1.1.1の罠
「INSTALL_REFERRERインテントブロードキャストが廃止される」という公式アナウンスがあった。
https://android-developers.googleblog.com/2019/11/still-using-installbroadcast-switch-to.html
広告系ライブラリを使っているとAndroidManifestに
INSTALL_REFERRER
がだいたい入ってるのため、対応しなきゃならない。対応方法
まずAndroidManifest.xmlから
INSTALL_REFERRER
を消す- <receiver> - <intent-filter> - <action android:name="com.android.vending.INSTALL_REFERRER" /> - </intent-filter> - </receiver>
次にapp/build.gradleに
com.android.installreferrer:installreferrer
を追加するのだが、、、
ここで自分は最新バージョンの1.1.1
を追加した。implementation 'com.android.installreferrer:installreferrer:1.1.1'このバージョンが失敗だった。
ここでトラップ発動
Android Studioの Analyze APK でAndroidManifestを見てみると、余計なパーミッションが追加されている。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />3つも!?
- WRITE_EXTERNAL_STORAGE
- READ_PHONE_STATE
- READ_EXTERNAL_STORAGE
特にREAD_PHONE_STATEが追加されたままリリースすると、ユーザーは驚くこと間違いなし
解決方法
installreferrer:1.0
を使うようにしよう。- implementation 'com.android.installreferrer:installreferrer:1.1.1' + implementation 'com.android.installreferrer:installreferrer:1.0'これで自動付与されるパーミッションは消えるはずだ
なぜ起きたのか?
おそらくGoogle側のバグで、過去に意図しないパーミッションが自動付与されることが何回か起きてるらしい。
https://developers.google.com/android/guides/releases
今後、ライブラリを更新したらパーミッションを確認したほうがいいかも(面倒くさい…)
- 投稿日:2020-02-21T08:19:48+09:00
【kotlin】自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する
自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する
最近、qiitaに寄稿していなかったので久々に寄稿。
Kotlin系の記事です。やりたかったこと
自作Twitterクライアントアプリ「TwitMorse 〜モールス信号でつぶやこう、あなたのSOSに誰かが答えてくれる〜」のAndroid版での挙動で、ハッシュタグに反応するLinkMovementMethodを実装して、ハッシュタグ検索結果画面へ遷移させよう、というものです。
- ハッシュタグの色付け
- ハッシュタグをタップしたらハッシュタグ専用画面への遷移
この時、kotlinのMatcherの挙動で苦労したので、後学のために残します。
修正前の実装(クラッシュ発生します)
以下、修正前のコード
/** * ハッシュタグを抽出してタップできるようにする * タップするとHashTagSearchResultActivityに遷移する * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。 */ fun onTapHashTag(text: String, textView: TextView) { textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要 val spannableString = SpannableStringBuilder(text) val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?") val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text) // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext hashTagRegex.findAll(text) .map { it.value } .forEach { val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java) hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it) hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK widget.context.startActivity(hashTagSearchResultActivity) } override fun updateDrawState(ds: TextPaint) { super.updateDrawState(ds) ds.isUnderlineText = false } } // ここが問題の箇所 spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableString } }ちょいと長めのコードで申し訳ないですが、
ポイントは2つ
val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?")
- 以下苦労した箇所
// ここが問題の箇所 spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableStringMatcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない
ハッシュタグの色付けには成功したものの、そのハッシュタグをタップすると下記例外でクラッシュします。
この修正に1日半かけて悩みました・・・。java.lang.IllegalStateException: No successful match so far修正前// ここが問題の箇所 spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableStringそんなMatchはおきねぇよ!!と。
そこで下記のように修正したら、正常に動作しました。
修正後if (matcher.find()) { spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableString }上記を見れば分かるように
matcher.find()
を呼んであげるとクラッシュがなくなりました。修正後の実装
/** * ハッシュタグを抽出してタップできるようにする * タップするとHashTagSearchResultActivityに遷移する * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。 */ fun onTapHashTag(text: String, textView: TextView) { textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要 val spannableString = SpannableStringBuilder(text) val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?") val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text) // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext hashTagRegex.findAll(text) .map { it.value } .forEach { val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java) hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it) hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK widget.context.startActivity(hashTagSearchResultActivity) } override fun updateDrawState(ds: TextPaint) { super.updateDrawState(ds) ds.isUnderlineText = false } } if (matcher.find()) { spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableString } } }これでmatcherの参照が動くためか、
java.lang.IllegalStateException: No successful match so far
のクラッシュは発生しなくなりまして、思い通りの挙動をしてくれるようになりました。複数ハッシュタグのリンク化
一つのTwitter投稿にハッシュタグが一つなわけがありません。下記やっておかないと、ひとつめのハッシュタグにしか反応しなくなるので、注意点として一応あげておきます
hashTagRegex.findAll(text).map { it.value }.forEach {}参考資料
- Kotlinのコーディングが捗る標準ライブラリ | qiita
- Android リアルタイム入力でハッシュタグ形式に文字装飾するTIPS | qiita
- 【kotlin】Matcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない | takelab.note
宣伝
- 投稿日:2020-02-21T01:51:52+09:00
Androidアプリ開発時にWiFi接続で無線デバッグ
Androidアプリの開発をしていると、実機確認する際にPCとAndroid端末が繋がっているのって邪魔じゃないですか?
私は邪魔でした!無線化しましょう!
環境はmac想定です。まずはadbを使えるように
Android端末をコマンドラインで操作するために、Android Debug Bridge(adb)を使います。
Android Studioを入れている場合はすでにadbが入っていると思うので、まずはパスを通します。bash_profileを編集しましょう。
vi ~/.bash_profileパスを追加します。
# 下記を追加 export PATH=$PATH:/Users/[ユーザー名]/Library/Android/sdk/platform-toolssource の再読込を行い適用。
source ~/.bash_profile
これでオッケー!
パスが追加されているかは下記コマンドで確認できます。echo $PATHAndroid端末を接続
Android端末とPCを同じWi-Fi環境に置いてください。
事前にAndroid端末のIPアドレスを確認しておきます。
[設定] -> [システム] -> [端末情報] -> [機器の状態] -> [IPアドレス] などで確認できます。Android端末とPCをケーブルで接続した状態で、下記を実行します。
adb tcpip 5555 adb connect xxx.xxx.xxx.xxx #ここにIPアドレス
これでケーブルを抜いても、デバッグできるようになります!
便利。接続を解除する場合は下記を実行。
adb disconnectせっかくなのでスクリプトも作るか
接続をひとつのコマンドで出来るようにしましょう。
てきとーにスクリプト入れる用のディレクトリを作ってパスを通しておきます。mkdir /User/[ユーザー名]/scripts
パスを追加
# 下記を追加 export PATH=$PATH:/Users/[ユーザー名]/scriptssource の再読込
source ~/.bash_profile
次にスクリプトを書くファイルを作成します。
ここで作成したファイル名が実行するコマンドになります。
ファイル名はadb connect
の略でadbc
としましたが、好きに決めてもらって大丈夫です。touch /User/[ユーザー名]/scripts/adbc vi /User/[ユーザー名]/scripts/adbc
作成した
adbc
ファイルの内容は下記のようにします。#!/bin/sh # サーバー再起動 adb kill-server && adb start-server # android端末に接続 adb tcpip 5555 && adb connect xxx.xxx.xxx.xxx #ここにIPアドレス行頭に
#!/bin/sh
を付ける必要があるため注意してください。
また、スクリプトを実行可能にするため、ファイルの権限編集を行います。chmod u+x /User/[ユーザー名]/scripts/adbc
ここまでで完了です!
Android端末をPCにつないで、ターミナルで下記を実行すれば接続されます!adbcちなみに、IPアドレスを動的に指定したい場合は、スクリプトを下記のように修正します。
#!/bin/sh # サーバー再起動 adb kill-server && adb start-server # android端末に接続 adb tcpip 5555 && adb connect $1これで実行時にIPアドレスを渡せるようになります。
adbc xxx.xxx.xxx.xxx #ここにIPアドレス
これでどこでも無線デバッグ!