diadia

興味があることをやってみる。自分のメモを残しておきます。

android facebookシェアのメモ

前提

intentやShareCompatを使ったシェア方法を諦め、facebook SDKを使ってシェアを行う。

メモ

とりあえず使うにはコーディングの他に準備がある。

まずFacebookに開発者の登録が必要

facebookにアプリの登録が必要 -> appのidがandroidマニフェストで記述する必要が出てくるから。

facebookのアプリの登録ページにはwebアプリの他にandroidアプリを作成する欄をボタンで設けることができる。

ここでキーハッシュなるものを登録する必要がある。 キーハッシュは2種類登録する。一つは開発者のキーハッシュ。これはアンドロイドスタジオ環境にはユニバーサルな値があるらしくそれを取得する。

またアンドロイドをリリースした際にはリリースしたキーも登録する。

この他にFacebook Activityを追加して、それをAndroidManifest.xmlに含めることもおこなう。

Android - Facebookログイン - ドキュメンテーション - Facebook for Developers

コーディング

facebookのシェアには種類がある。当初シェアしたい画像とともにリンクをシェアしたいと思っていたが、ルールが有るようだ。

  • リンクのシェア
  • 画像のシェア
  • 動画のシェア
  • 画像、動画が混在するマルチメディアのシェア

これらのどれに該当するかでコードが変わってくる?変わってくる.

リンクシェア
ShareLinkContent content = new ShareLinkContent.Builder()
        .setContentUrl(Uri.parse("https://developers.facebook.com"))
        .build();



写真
Bitmap image = ...
SharePhoto photo = new SharePhoto.Builder()
        .setBitmap(image)
        .build();
SharePhotoContent content = new SharePhotoContent.Builder()
        .addPhoto(photo)
        .build();


動画
Uri videoFileUri = ...
ShareVideo = new ShareVideo.Builder()
        .setLocalUrl(videoUrl)
        .build();
ShareVideoContent content = new ShareVideoContent.Builder()
        .setVideo(video)
        .build();


マルチメディア
SharePhoto sharePhoto1 = new SharePhoto.Builder()
    .setBitmap(...)
    .build();
SharePhoto sharePhoto2 = new SharePhoto.Builder()
    .setBitmap(...)
    .build();
ShareVideo shareVideo1 = new ShareVideo.Builder()
    .setLocalUrl(...)
    .build();
ShareVideo shareVideo2 = new ShareVideo.Builder()
    .setLocalUrl(...)
    .build();

ShareContent shareContent = new ShareMediaContent.Builder()
    .addMedium(sharePhoto1)
    .addMedium(sharePhoto2)
    .addMedium(shareVideo1)
    .addMedium(shareVideo2)
    .build();

ShareDialog shareDialog = new ShareDialog(...);
shareDialog.show(shareContent, Mode.AUTOMATIC);

リンクのシェアはちゃんとurlの他に画像も表示される仕組みのようだからリンクシェアで良いと思われる。

とりあえずShareLinkContentに、シェアしたいリンクをsetContentUrlメソッドを実行して格納したオブジェクトを生成する。 これはandroidのクライアント側でpostを実行し、レスポンスとしてurlを返せばこのオブジェクトは使える。retrofitのonResponseメソッド内に挿入すればよいだろう。

ShareLinkContent content = new ShareLinkContent.Builder()
        .setContentUrl(Uri.parse("https://developers.facebook.com"))
        .build();

このオブジェクトをシェアするにはシェアインターフェースの実装が必要になるそうだ。 このインターフェースには種類があり、ボタンとシェアダイアログ、メッセージダイアログが存在する。

現在のsdkはシェアダイアログを開く際にユーザー端末のfacebookアプリが存在するか確認する必要がなくなったそうだ。 昔はシェアする必要があった。

この他にシェアリンクをクリックしたらアプリが自動起動する仕組みも実装することができるようだ。app linkという機能のようだ。

シェアボタンはボタンを押すとシェアダイアログを起動する機能のボタンである。

Seleniumを使ったテストのメモ

実装の際に困ったこと

  • django.test.LiveServerTestCaseを使う環境を把握できていなかった。

  • ヘッドレスモードを使う場合にはオプションに--window-sizeをつけなければならないこと。

前提

ChromeDriverを使ってseleniumを実行する

準備

seleniumをインストールする。
オプションでwebdrivermanagerをインストールしても良い。

pip install selenium

webdrivermanagerをインストールしても良い。これはseleniumが扱うChromeDriverのバージョンが異なるとエラーが出る?ようで、webdrivermanagerを使うと必要な適切なDriverをその場でインストールしてseleniumを実行するもの。

pip install webdrivermanager

LiveServerTestCaseのメモ

LiveServerTestCaseは通常のテストと同じようにassertを使ってテストする。またテストの始め方も

python manage.py test 
#または
python manage.py test app.tests

で実行する。しかしながら前提としてアプリケーションサーバーを事前に起動しておく必要がある。これを行っていないとエラーが発生してしまう。 またアプリケーションサーバーのuriも同一のところにしないと当然ながらエラーが出てしまう。
アンドロイドを実機で開発しているとipを使ってrunserverするので、不一致が生じてしまうので注意すること。

seleniumを使ったテストを実行する際の注意点の資料

python - django selenium fails to load localhost - Stack Overflow

ヘッドレスモードでセレニウムを使う

    def setUp(self):

        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument('--headless')
        chrome_options.add_argument('--window-size=1920,1080') 
        self.driver = webdriver.Chrome(ChromeDriverManager().install(), chrome_options=chrome_options)

ヘッドレスでテストを行うと画面上にブラウザが表示されないのがメリット。 ヘッドレスで起動するにはオプションを追加すれば使える。ChromeOptions()インスタンスを生成し、インスタンスにadd_argument('--headless')メソッドと引数を与えてからwebdriver.Chromeインスタンスを生成すれば良い。

別のオプションとして'--window-size=1920,1080'を追加している。これはオプションを使わずに.click()メソッドを使ってもクリックできない場合があって、それに対応するためのもの。

ヘッドレスモードでクリックが反応しない場合の対処に関する資料

python - Is it possible to use `element.click()` on Selenium with Chrome even on headless mode? - Stack Overflow

djnago test ModelFormにForeignKeyが含まれる場合のテスト

例えばItemモデルにCategoryというForeignKeyが含まれているとする。 その上ForeignKeyオブジェクトから選択させる方式でItemオブジェクトを生成している。

Itemオブジェクト生成のためにItemModelFormを作成すると以下のような感じになる。

class ItemModelForm(forms.ModelForm):
    category = forms.ModelChoiceField(queryset=Category.objects.all())

    class Meta:
        model  = Item
        fields = ('category', 'title', 'price','description',  )

このModelFormには一癖ありテストに手こずったのでメモしておく。

ModelChoiceFieldを含むModelFormの挙動について。

ユーザーがModelChoiceFieldから特定のオブジェクトを選択すると、当然ながらModelFormのオブジェクト内にForeignKeyのオブジェクトが格納されてform.is_valid()処理が実行される。 しかしながら手動で同じ環境を作るときは注意が必要であった。ForeignKeyはForeignKeyのオブジェクトのidとしてModelFormに格納されている。したがって以下のようにしてもエラーがずっと出続けるだけで一向に進めないのだ。

category_obj = Category.objects.get(id=2)
data = {
             'category':categoy_obj, 
             'title': "hduhig", 
             'price':100,
             'description':"hsudahisug"
             }

form = ItemModelForm(data)
print(form.is_valid())
>>> False

こうする。 要するにModelFormにForeignKeyを格納する際にはForeignKeyのid値で入れること。

category_obj = Category.objects.get(id=2)
data = {
             'category':categoy_obj.id, 
             'title': "hduhig", 
             'price':100,
             'description':"hsudahisug"
             }

form = ItemModelForm(data)
print(form.is_valid())
>>> True

理由はよくわからないがSerializerのところでもForeignKeyの表示はid値で表現されている。FormのようなタイプにおいてはForeignKeyはid値が扱われる仕様になっているのであろう。

その他djangoのテストに関連するもの

Djangoのテストを実装 - diadia

WebViewの使い方

コンテンツ

  1. ドキュメント
  2. 実装方法
  3. サンプルコード

1.ドキュメント

WebView でのウェブアプリの作成  |  Android デベロッパー  |  Android Developers

2.実装手順

レイアウトでアプリに WebView を追加

<WebView
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>
val myWebView: WebView = findViewById(R.id.webview)
    myWebView.loadUrl("http://www.example.com")

マニフェストファイルにパーミッションを記述する

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

基本的な実装はこれで完結する。

JavaScript を有効にする

val myWebView: WebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true

3.サンプルコード

GitHub - chiaki1990/WebViewSample

DRFとretrofitの関係(Bodyに関して)

話したいこと

drfのrequest.dataの内容はfieldかbodyかで違いが出る話をしたい。

django rest frameworkを使ってアンドロイドからデータを受け取る場合にはrequest.dataを使う。そしてこのrequest.dataの内容がretrofitのインターフェースメソッドの記述の仕方によって変わることをメモしておく。

retrofitで@Bodyを使った場合にrequest.dataはどのように表示されるか

@POST("api/util/region/")
fun postGetRegionDataByPointAPIView(
    @Header("Authorization") authTokenHeader: String?, 
    @Body wkt_point: String
):Call<ResultRegionModel>

上記のように@Bodyを使ってdjangoに送信した場合には、以下のようにデータを受け取る。

SRID=4326;POINT (-91.51643849909306 14.84320911634316)

retrofitで@Fieldを使った場合にrequest.dataはどのように表示されるか

@FormUrlEncoded
@POST("api/util/region/")
fun postGetRegionDataByPointAPIView(
    @Header("Authorization") authTokenHeader: String?, 
    @Field("wkt_point") wkt_point: String
):Call<ResultRegionModel>

上記のように@Fieldを使ってdjangoに送信した場合のデータは以下である。

<QueryDict: {'wkt_point': ['SRID=4326;POINT (-91.50813136249779 14.84655173537646)']}>

この違いからdjangoで何が変わってくるのか?

送信されたデータの取り出し方が変わってくる。Bodyの場合にはrequest.dataでデータを受け取ることができるが、 Fieldの場合はrequest.dataがdict型なのでrequest.data["wkt_point"]を使ってデータを受けとる事ができる。

Kotlin GoogleMapPlatfotmのtoolbarを削除する

どれ?

f:id:torajirousan:20200529065852p:plain

toolbarの特徴

toolbarはデフォルトで表示され、toolbarをタップすると他のアプリが起動してしまう。 消したい場合はドキュメントに従ってUiSettings.setMapToolbarEnabled(boolean)を実行すれば良い。

サンプルコード

override fun onMapReady(googleMap: GoogleMap?) {

    googleMap!!.uiSettings.isMapToolbarEnabled = false
       
}

toolbarに関するドキュメント

コントロールと操作  |  Maps SDK for Android  |  Google Developers

UiSettings.setMapToolbarEnabledに関するドキュメント

UiSettings  |  Android 用 Google API  |  Google Developers

Django シグナル POST_SAVEのupdate_fieldsをどう使うか

シグナルpost_saveのupdate_fieldsの用途

例えばこんな時に使いたい時にupdate_fieldsが役に立つ。

class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
    adm0 = models.CharField(max_length=15, default="Japan", choices=adm0_CHOICES)
    adm1  = models.CharField(max_length=15, null=False, choices=DEPARTAMENTO_CHOICES)
    adm2 = models.CharField(max_length=30, null=False, choices=MUNICIPIO_CHOICES)
    description  = models.TextField(default=DEFAULT_PROFILE_DESCRIPTION)
    point = geomodels.PointField(null=True, blank=True)

Ptofileオブジェクトには国の属性を示すadm0、都道府県の属性を示すadm1、市を示すadm2が存在する。一方でユーザーの座標情報を格納するpoint(ジオメトリ型データ)が存在する。

ユーザーのProfileオブジェクトはユーザーの好みによって2種類の生成もしくは変更方法がある。

  • ユーザーが都道府県、市を各リストから選択して登録する方法
  • ユーザーが位置情報を入力し、webアプリが位置情報に合致した都道府県、市を探し登録する方法

後者のProfileオブジェクトのpointが変更されたときだけ、シグナルが発行され、座標データに従って都道府県、市を変更するを実現する場合には、post_saveのcreatedだけではうまく作動させることができない。しかしpost_saveシグナルが発火し、かつpointが変更されたという状況にのみ都道府県、市を変更するコードが動くとしたらやりたいことを実現できる。そしてこれはupdate_fieldsを使うなら実現できる。

イメージサンプルコード

def hoge(sender, instance, created, update_fields, *args, **kwargs):
    if created == True:
        return 
    elif created == False and "point" in  update_fields:
        #point属性値に従ってadm1, adm2を変更するロジックを書く
        return 

post_save.connect(hoge, sender=Profile)

上記のようにコードを記述するだけではシグナルがうまく作動しない。
上記のコードには誤りはないのだけれども、これだけでは足りないのである。

Noneの値が常に出てしまう問題

イメージサンプルコードを走らせるとupdate_fieldsの値がことごとくNoneとなってしまうのである。 したがって期待するロジックが実行されない問題に直面することになる。これはupdate_fieldsの使い方に問題があるからだ。

じゃあupdate_fieldsをどう使うべきなのか?

update_fields(シーケンス型のデータ)はコード記述者が自ら要素を格納しないといけない。 具体的にはあるクラスオブジェクトのsave()メソッド実行時にupdate_fieldsを使うのである。

profile_obj = Profile.objects.get(id=2)
profile_obj.point = point 
profile_pbj.save(update_fields=["point"])

上記のようにオブジェクトの更新に際してupdate_fields=["point"]をsave()メソッドの引数とするのである。
こうすると晴れてシグナルのupdate_fieldsの値はNoneではなく、"point"に関連したデータになるのである。

しかし問題点も存在する。上記の方法ではviews.pyのロジック中に記述しているのだけれども、例えばモバイルアプリケーションを通じてオブジェクトの変更する場合にはロジック中に記述する事ができないのである。 それはモバイルアプリを通じてデータを受信する場合にDRFを使うとSerializerを使ことになるからである。この場合にはserializer.save()を使うのだけれどもこのsave()はupdate_fields=["point"]を許容することができないのだ。 この場合には例えば以下のようにして対応する。

serializer = ProfileSerializer(profile_obj, data=request.data)
if serializer.is_valid():
    if "point" in request.data.keys():
        #シグナルを実行するためにmodel.save()を実行する
        serializer.save()
        serializer.instance.save(update_fields=["point"])
    else:
        serializer.save()

FirebaseCloudMessagingによるプッシュ通知を受信してもアプリを起動できない件

理由がわからない。 サンプルで作ったアプリはプッシュ通知をタップするとアプリが立ち上がったけど、今のアプリは立ち上がらない。

Firebase Notifications は、受信側アプリがフォアグラウンド状態であるかバックグラウンド状態であるかによって、動作が異なります。

フォアグラウンドって具体的にどんな状態を示すのか分かっていない。

フォアグラウンドとバックグラウンドで設定する内容が異なるようだ。
フォアグラウンドの場合には以下が必要。

<service
     android:name=".MyFirebaseMessagingService">
     <intent-filter>
         <action android:name="com.google.firebase.MESSAGING_EVENT"/>
     </intent-filter>
</service>

それでMyFirebaseMessagingServiceのサービスを作成すれば良い。

バックグラウンド用には、

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="@string/default_notification_channel_id"/>
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
     See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_launcher_foreground" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
     notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

<string name="default_notification_channel_id" translatable="false">my_app_fcm_default_channel</string>

これらが必要になる。

テスト(ソフトウェアテスト)について

ソフトウェアテストとは、開発者の意図したとおりにソフトウェアが動作するかを検証する行為である。

ソフトウェアテストには種類がある。

テスト名 説明
ユニットテスト メソッド単位のテスト
統合テスト メソッドの組み合わせのテスト
UI/システムテスト データベースやUI部品を組み合わせたテスト

ソフトェアが意図したとおり動作して得られる値を期待値と言い、ソフトウェアが実際に動作して得られた値を実測値という。 そしてこの期待値と実測値の比較検証のことをアサーションと呼ぶ。

ソフトウェアテストを区分するのにブラックボックス/ホワイトボックステストというくくりも存在する。

テスト種類 説明 特徴 / デメリット
ホワイトボックステスト 内部のロジックや仕様を考慮してテストケースを作成する。 ロジックなどを読み取れる必要があるので、ある程度プログラミングの知識が必要 / テストコードが内部構造に強く依存してしまうと、テストコードがプロダクトコードの変更に影響を受けてしまう
ブラックボックステスト ソフトウェアの内部のロジックや仕様を考慮せず、外部から見たときの仕様のみからテストケースを作成する プログラミングの知識よりも業務に関する知識が必要 / 入出力は単純だが内部構造が複雑なものをテストできない

#

ありがちなアンチパターンはすべてのテストを一つのツールでやってしまおうとすること。ツールによって得意不得意があるのでツールの併用を行う方が良いらしい。

スワイプでデータ更新をする

SwipeRefreshLayoutを使えば良い事がわかった。 GlidやLinearの親としてSwipeRefreshLayoutを使う。

参考ドキュメント

スワイプでの更新をアプリに追加する  |  Android デベロッパー  |  Android Developers

参考ドキュメントによるとsupport.v4を使うことになっているが、androidxのバージョンを使いたい。 そのためにimport先を調べておく必要がある。

データの更新が終了したらsetRefreshing(false)をセットする事がわかった。

使い方のイメージをメモ

まずSwipeRefreshLayoutはデフォルトで使えない機能なのでandroidxのモジュールをgradleにインストールする。

//swiperefreshlayoutのインストール : 
//https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'

次にlayoutファイルにandroidx.swiperefreshlayout.widget.SwipeRefreshLayoutビューを加える。このタグ以下にはListViewやRecyclerViewを設置するが、複数のビューを設置する事ができないので気をつけること。

このビューにはsetOnRefreshListener{}がある。スワイプを検知するリスナーで{}内にデータ更新のロジックを記述する。 データ更新が終わったらクルクルを削除するためにswiperefresh.isRefreshing = falseを実行する。 コレで良い。

RecyclerViewでスクロールダウンしてから上に戻ると表示がおかしくなる件

リサイクラービューで上から下にスクロールする時には適切に表示されているのに、 一度下までスクロールして上に戻ると、適切に表示されていたものが変な状態で表示される件に遭遇した。

具体的に自分のケースでは、if文に合致したあるアイテムはグレースケールで画像を表示されるのに下までスクロールして上げてみると別のアイテムがグレースケールの画像になっていた。

コレについてはAdapterに

   override fun getItemId(position: Int): Long {
        //return super.getItemId(position)
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        //return super.getItemViewType(position)
        return position
    }

コレを追加することで挙動を適正に戻すことができた。理由はドキュメントにあると思うので時間のある時にじっくり調べてみたい。

自分と同じケースに陥ってしまった場合の対処に関する資料

java - RecyclerView messed up data when scrolling - Stack Overflow

Glideの使い方

内容

  1. 普通にGlideを使いたい場合 -> Glideを使う場合の諸設定
  2. GlideAppを使いたい場合 -> GlideAppを使いたい場合の諸設定

Glideを使う場合の諸設定

gradleに以下を設定

dependencies {
  implementation 'com.github.bumptech.glide:glide:4.11.0'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}

(補足)GlideAppを使いたい場合には、annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'を記述しないでおくこと。

レイアウトファイルにImageViewを追加する。

        <ImageView
        android:id="@+id/show_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>   

アクティビティでImageViewオブジェクトを取得し、Glideを使ってImageViewに画像を表示する。

override fun onCreate(savedInstanceState: Bundle?) {

...割愛

// ImageViewオブジェクトを取得する
val iv_show_image = findByIdView&lt;ImageView&gt;(R.Id.show_image)

// 表示したい画像urlをString型データで変数定義する
val urlString = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Kotlin-logo.svg/144px-Kotlin-logo.svg.png"

// Glideライブラリを使用する
Glide.with(activity:this).asBitmap().load(urlString).into(iv_show_image)

}

GlideAppを使いたい場合の諸設定

GlideAppはcircleCrop()等のGlideでは使えない機能を使う場合に使われるモジュールである。 これを使うと画像を単に表示するだけでなく、画像を丸くくり抜いて表示したり編集された状態で画面表示する事ができるようになる。 JavaとKotlinではGlideAppを使う諸設定に違いがあったのでメモしておく。

gradleに以下を設定

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0'
}

このままではkaptでエラーが出てしまうので同ファイル(appレベルのgradleファイル)に

apply plugin: 'kotlin-kapt'

を追加する。こんな感じで、

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29

...省略

次に適当なファイルにAppGlideModuleを拡張したクラスを定義する。 例えばMyAppGlideModule.ktというのを作って、MyAppGlideModuleを定義する。

package com.example.hoge


import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule

@GlideModule
class MyAppGlideModule : AppGlideModule(){

}

その後にBuildメニューのRebuildを実行する。
するとGlideAppというモジュールを使うことができるようになっている。

GlideApp.with(this).load(imageUrl).circleCrop().into(myImageView);

KotlinでGlideAppを使う場合の手順を説明した資料

Using Glide with Kotlin - Vlonjat Gashi - Medium

Java版GlideAppを使う場合の手順を説明した資料

Android用画像読み込みライブラリ、Glideを使ってみよう! | 株式会社ヌーラボ(Nulab inc.)

KotlinでGlideAppを導入できない問題に出くわした場合の対処に関する資料

cannot resolve symbol 'GlideApp' (GlideApp was not generated) · Issue #1945 · bumptech/glide · GitHub

Glideドキュメント

GitHub - bumptech/glide: An image loading and caching library for Android focused on smooth scrolling

Android端末の通信中にクルクルを表示したい

通信中にクルクルを表示するための機能はProgressDialogとProgressBarがあるようだ。

ProgressDialogはAPI26で非推奨になるらしい。
ProgressBarを使うことになるのか?

2種類あるみたい。 Determinate ProgressBar Indeterminate ProgressBar