Quantcast
Channel: Y.A.M の 雑記帳
Viewing all 415 articles
Browse latest View live

AutoValue ライブラリを試してみた

$
0
0
https://github.com/google/auto/blob/master/value/userguide/index.md

immutable value class を生成してくれるライブラリ。abstract クラスを用意して @AutoValue をつけると、equals() や hashCode() などの boilerplate なコードを実装したクラスを用意してくれる。

設定

dependencies { compile 'com.google.auto.value:auto-value:1.2' apt 'com.google.auto.value:auto-value:1.2' }

使い方

例えば @AutoValue abstract class Animal { abstract String name(); abstract int numberOfLegs(); } のようなクラスを定義すると、AutoValue_Animalというクラスが生成される。 final class AutoValue_Animal extends Animal { private final String name; private final int numberOfLegs; AutoValue_Animal( String name, int numberOfLegs) { if (name == null) { throw new NullPointerException("Null name"); } this.name = name; this.numberOfLegs = numberOfLegs; } @Override String name() { return name; } @Override int numberOfLegs() { return numberOfLegs; } @Override public String toString() { return "Animal{" + "name=" + name + ", " + "numberOfLegs=" + numberOfLegs + "}"; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof Animal) { Animal that = (Animal) o; return (this.name.equals(that.name())) && (this.numberOfLegs == that.numberOfLegs()); } return false; } @Override public int hashCode() { int h = 1; h *= 1000003; h ^= this.name.hashCode(); h *= 1000003; h ^= this.numberOfLegs; return h; } } コンスタクタでは name と numberOfLegs を引数に取り、equals() や hasCode() ではこれらを使った実装になっている。 immutable value class なのでコンスタクタで受け取った引数は final として保持される。

abstract クラスに static な生成メソッドを用意して利用する。 @AutoValue abstract class Animal { static Animal create(String name, int numberOfLegs) { return new AutoValue_Animal(name, numberOfLegs); } abstract String name(); abstract int numberOfLegs(); } コンスタクタの引数は null チェックされる。これを止めたいときは abstract メソッドの戻り値に @Nullable をつける。 @AutoValue abstract class Animal { @Nullable abstract String name(); abstract int numberOfLegs(); } final class AutoValue_Animal extends Animal { private final String name; private final int numberOfLegs; AutoValue_Animal( @Nullable String name, int numberOfLegs) { this.name = name; this.numberOfLegs = numberOfLegs; } ... } Builder を用意することも可能。@AutoValue をつけるクラスにインナークラスとして abstract static な Builder クラスを定義し、@AutoValue.Builder をつける。 @AutoValue abstract class Animal { abstract String name(); abstract int numberOfLegs(); static Builder builder() { return new AutoValue_Animal.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder name(String value); abstract Builder numberOfLegs(int value); abstract Animal build(); } } final class AutoValue_Animal extends Animal { ... static final class Builder extends Animal.Builder { private String name; private Integer numberOfLegs; Builder() { } Builder(Animal source) { this.name = source.name(); this.numberOfLegs = source.numberOfLegs(); } @Override public Animal.Builder name(String name) { this.name = name; return this; } @Override public Animal.Builder numberOfLegs(int numberOfLegs) { this.numberOfLegs = numberOfLegs; return this; } @Override public Animal build() { String missing = ""; if (name == null) { missing += " name"; } if (numberOfLegs == null) { missing += " numberOfLegs"; } if (!missing.isEmpty()) { throw new IllegalStateException("Missing required properties:" + missing); } return new AutoValue_Animal( this.name, this.numberOfLegs); } } }

BehaviorSubject を使って Activity と Fragment のデータの読み込みを待ち合わせる

$
0
0
画面構成は
  • MainActivity
    • TabLayout + ViewPager
    • ViewPagerの各ページは MainFragment
  • MainFragment
    • RecyclerView

やりたいことは
  • MainActivity
    • 各ページで共通のデータ(以後 CommonData)をサーバーから取得する
  • MainFragment
    • ページ特有のデータ(以後 SpecificData)をサーバーから取得する
    • MainActivity から CommonData をもらう
    • CommonData と SpecificData 両方の読み込みが終わったら RecyclerView にデータを追加する

キモになるのが、CommonData と SpecificData 両方の読み込みが終わるのを待ち合わせたいというところです。
CommonData の読み込みが終わる前に生成された MainFragment なら両方の読み込みを待ち合わせるし、 CommonData の読み込みが終わった後に生成された MainFragment なら SpecificData の読み込みだけ待てばいいわけです。
でも、この2つの状態をわけて処理すると煩雑になってしまいます。

そこで、BehaviorSubjectを使って、CommonData の値を MainFragment 側に渡せるようにします。 BehaviorSubject は onNext() が呼ばれたときにその値を通知し、さらにその値をキャッシュします。新しく subscribe されると、最新のキャッシュした値があればその時点で通知します。

public class MainActivity extends AppCompatActivity implements MainFragment.MainFragmentListener { private final BehaviorSubject<CommonData> commonDataBehaviorSubject = BehaviorSubject.create(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... loadCommonData(); } private void loadCommonData() { subscription = DataRetriever.getInstance().getCommonData() .onErrorReturn(new Func1<Throwable, CommonData>() { @Override public CommonData call(Throwable throwable) { // エラーのときはデータがないものとして扱う return CommonData.empty(); } }) .subscribe(new Action1<CommonData>() { @Override public void call(CommonData commonData) { commonDataBehaviorSubject.onNext(commonData); } }); } @NonNull @Override public Observable<CommonData> getCommonDataObservable() { return commonDataBehaviorSubject; } } public class MainFragment extends Fragment { public interface MainFragmentListener { @NonNull Observable<CommonData> getCommonDataObservable(); } @Nullable private MainFragmentListener listener; ... @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof MainFragmentListener) { listener = (MainFragmentListener) context; } } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (adapter == null) { adapter = new DataAdapter(); load(); } recyclerView.setAdapter(adapter); } void load() { recyclerView.setVisibility(View.GONE); progressView.setVisibility(View.VISIBLE); // 共通データとタブ独自のデータ両方揃うまで待ち合わせ subscription = Observable .combineLatest( getCommonDataObservable(), getSpecificDataObservable(), new Func2<CommonData, SpecificData, Pair<CommonData, SpecificData>>() { @Override public Pair<CommonData, SpecificData> call(CommonData commonData, SpecificData specificData) { return new Pair<>(commonData, specificData); } }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<Pair<CommonData, SpecificData>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { recyclerView.setVisibility(View.VISIBLE); progressView.setVisibility(View.GONE); } @Override public void onNext(Pair<CommonData, SpecificData> combinedData) { recyclerView.setVisibility(View.VISIBLE); progressView.setVisibility(View.GONE); final List<String> list = new ArrayList<>(); final CommonData commonData = combinedData.first; list.add("CommonData : " + (commonData.isEmpty() ? "empty" : commonData.getData())); final SpecificData specificData = combinedData.second; list.addAll(specificData.getData()); adapter.addAll(list); } }); } /** * 共通のデータを取得 */ private Observable<CommonData> getCommonDataObservable() { return listener != null // first() を介して onComplete()が呼ばれるようにしている ? listener.getCommonDataObservable().first() : Observable.just(CommonData.empty()); } /** * このタブ独自のデータを取得 */ private Observable<SpecificData> getSpecificDataObservable() { final int position = getArguments() == null ? -1 : getArguments().getInt(ARGS_POSITION); return DataRetriever.getInstance().getSpecificData(position); } ... }

さらに、MainActivity に SwipeRefreshLayout を追加して、PullToRefresh で共通データを取り直し、各 MainFragment にもデータを取り直させる処理を追加したサンプルが
https://github.com/yanzm/BehaviorSubjectSample
です。


Fragment に Toolbar を持たせるのはやめなさい

$
0
0
NavigationDrawer や BottomNavigation パターンを実現するために、各画面を Fragment で実装することがあります。 Fragment によって ActionBar に持たせる機能が違うからか、Fragment のレイアウトに Toolbar を持たせて、Fragment で ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); のような処理をさせているコードを見かけることがあります。

やめなさい

(Toolbar をただの View として使って、ActionBar としては使わない(setSupportActionBar()しない)というのであればまだ許容できるが、それならそもそも Toolbar を使う必要がない)

ViewPager のように複数の Fragment を一度に attach する場合、これでは予期しない動作になることがありえます。ちゃんと Fragment に用意されている機能を使ってください。 Fragment で setHasOptionsMenu(true) を呼ぶと onCreateOptionsMenu() が呼ばれるので、Fragment 用の Menu を inflate します。

ViewPager はこの機能を適切に処理しており、現在のページの Fragment の Menu だけ inflate されるようになっています。 また、FragmentTransaction の show() / hide() で Fragment の表示・非表示を切り替える際も OptionsMenu であれば一緒に適切に処理されます。


Fragment にこのような機能があることを知っているにもかかわらず、上記のようなひどいコードを実装してしまう要因として、Menu ではなく View を置きたいという状況があります。
よくあるのが ActionBar に検索用の入力フィールド(EditText)を持たせたい場合です。
Menu ではなく View を置きたいのだから OptionsMenu の機能は使えないと思ってしまうのでしょうか。

OptionsMenu にはこのような用途のために ActionView という機能があります。MenuItem に独自のレイアウト/Viewを設定できる機能です。 Menu リソースの item で android:actionLayout(app:actionLayout)を使ってレイアウトを指定することもできます。
また、android:actionViewClass(app:actionViewClass) で View クラスを指定することもできます。

この ActionView 用に用意されているクラスとして SearchViewがあります。 SearchView は ActionBar に検索用の入力フィールド(EditText)を持たせてくれるそのものずばりの機能です。文字が入力されているときにクリアボタン(xボタン)が出る機能も実装されています。 これを利用せずにわざわざ自分で実装する意味はあまりないと思いますが、独自でやりたいのであればそれ用のViewクラスを自分で用意して android:actionViewClass で指定すればよいのです。

まとめると、
  • Fragment のレイアウトに toolbar を持たせない
  • Fragment 独自の機能を ActionBar に入れたいときは OptionsMenu の機能を使う
  • OptionsMenu の機能なら ViewPager で適切に処理される
  • OptionsMenu の機能なら FragmentTransaction の show() / hide() で適切に処理される
  • OptionsMenu には独自の View を配置できる ActionView 機能がある


追記1

Toolbar の中に複雑な View を入れること自体をダメだと言っているわけではありません。 Activityのレイアウトで <android.support.v7.widget.Toolbar> に子ビュー持たせるのは別にいいと思います。 このエントリはあくまで Fragment に toolbar を持たせることについての話です。

RecyclerView のクリックをどこで処理するか

$
0
0
RecyclerView.Adapter の責務はデータと ViewHolder の紐付けなので、View がタップされたときの処理(リスナーをセットすることではない)を Adapter 内に書くべきではないと思っています。

ではどうしているかというと、View がタップされたときに呼び出すメソッドを Adapter 内に定義しておき、Activity や Fragment で Adapter を生成するときにそのメソッドを Override してタップされたときの処理を記述するようにしています。 public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); final VersionAdapter adapter = new VersionAdapter() { @Override protected void onVersionClicked(@NonNull String version) { super.onVersionClicked(version); // Activity 側でタップされたときの処理を行う Toast.makeText(MainActivity.this, version, Toast.LENGTH_SHORT).show(); } }; recyclerView.setAdapter(adapter); } public static class VersionAdapter extends RecyclerView.Adapter<VersionViewHolder> { // タップされたときに呼び出されるメソッドを定義 protected void onVersionClicked(@NonNull String version) { } private final List<String> versions = new ArrayList<>(); @Override public VersionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final VersionViewHolder holder = VersionViewHolder.create(inflater, parent); // onCreateViewHolder でリスナーをセット holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final int position = holder.getAdapterPosition(); final String version = versions.get(position); onVersionClicked(version); } }); return holder; } ... } } 完全なサンプルは
https://github.com/yanzm/RecyclerViewSample
にあります。


ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する

$
0
0
ViewAnimationUtils.createCircularReveal()

ViewAnimationUtils.createCircularReveal()は、Viewを円形にくり抜くアニメーション(Animator)を作るユーティリティメソッドです。 例えばこんな感じ。 public class SampleActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sample); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final View container = findViewById(R.id.container); final int width = container.getWidth(); final int height = container.getHeight(); float startRadius = (float) Math.sqrt(width * width + height * height) / 2; float endRadius = 0; final Animator animator = ViewAnimationUtils.createCircularReveal(container, width / 2, height / 2, startRadius, endRadius); animator.setDuration(3000); animator.start(); } }); } }



FAB の transforming

https://material.google.com/components/buttons-floating-action-button.html#buttons-floating-action-button-transitionsの真ん中あたり、toolbar という項目のやつです。

アニメーション以外の本質的なコードは toolsContainer と fab の visibility の切り替えだけ(以下の部分)なんですけど、アニメーションのコード入れると長い... toolsContainer.setVisibility(View.VISIBLE); fab.setVisibility(View.INVISIBLE); これが全体のコードなのですが、長いですね... public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final View toolsContainer = findViewById(R.id.tools_container); final View tools = findViewById(R.id.tools); final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); final ToggleButton toggleButton = (ToggleButton) findViewById(R.id.button); toggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { final int fabWidth = fab.getWidth(); final int fabHeight = fab.getHeight(); final int toolsWidth = toolsContainer.getWidth(); final int toolsHeight = toolsContainer.getHeight(); float startRadius = fabHeight / 2f; float endRadius = (float) (Math.sqrt(toolsWidth * toolsWidth + toolsHeight * toolsHeight)); int[] outLocation = new int[2]; toolsContainer.getLocationInWindow(outLocation); int[] fabOutLocation = new int[2]; fab.getLocationInWindow(fabOutLocation); float diff = isChecked ? (outLocation[1] + toolsHeight / 2) - (fabOutLocation[1] + fabHeight / 2) : 0; int centerX = (int) (fabOutLocation[0] + fabWidth / 2 - outLocation[0] - diff); int centerY = toolsHeight / 2; final int FAB_DURATION = 100; final int TOOLS_DURATION = 300; if (isChecked) { final Animator fabAnimator1 = ObjectAnimator.ofFloat(fab, "translationY", diff); fabAnimator1.setDuration(FAB_DURATION); fabAnimator1.setInterpolator(new DecelerateInterpolator()); final Animator fabAnimator2 = ObjectAnimator.ofFloat(fab, "translationX", -diff); fabAnimator2.setDuration(FAB_DURATION); fabAnimator2.setInterpolator(new AccelerateInterpolator()); final ValueAnimator fabAnimator3 = ValueAnimator.ofInt(255, 0); fabAnimator3.setDuration(FAB_DURATION); fabAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final int alpha = (int) animation.getAnimatedValue(); final Drawable drawable = fab.getDrawable(); drawable.setAlpha(alpha); } }); final Animator toolsContainerAnimator = ViewAnimationUtils.createCircularReveal(toolsContainer, centerX, centerY, startRadius, endRadius); toolsContainerAnimator.setDuration(TOOLS_DURATION); toolsContainerAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); toolsContainer.setVisibility(View.VISIBLE); fab.setVisibility(View.INVISIBLE); } }); tools.setPivotX(centerX); final Animator toolsAnimator = ObjectAnimator.ofPropertyValuesHolder(tools, PropertyValuesHolder.ofFloat("alpha", 0f, 1f), PropertyValuesHolder.ofFloat("scaleX", 0.8f, 1f)); toolsAnimator.setDuration(TOOLS_DURATION); AnimatorSet set = new AnimatorSet(); set.play(toolsContainerAnimator).with(toolsAnimator) .after(fabAnimator1).after(fabAnimator2).after(fabAnimator3); set.start(); } else { final Animator fabAnimator1 = ObjectAnimator.ofFloat(fab, "translationY", 0); fabAnimator1.setDuration(FAB_DURATION); fabAnimator1.setInterpolator(new AccelerateInterpolator()); final Animator fabAnimator2 = ObjectAnimator.ofFloat(fab, "translationX", 0); fabAnimator2.setDuration(FAB_DURATION); fabAnimator2.setInterpolator(new DecelerateInterpolator()); final ValueAnimator fabAnimator3 = ValueAnimator.ofInt(0, 255); fabAnimator3.setDuration(FAB_DURATION); fabAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final int alpha = (int) animation.getAnimatedValue(); final Drawable drawable = fab.getDrawable(); drawable.setAlpha(alpha); } }); final Animator toolsContainerAnimator = ViewAnimationUtils.createCircularReveal( toolsContainer, centerX, centerY, endRadius, startRadius); toolsContainerAnimator.setDuration(TOOLS_DURATION); toolsContainerAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); toolsContainer.setVisibility(View.INVISIBLE); fab.setVisibility(View.VISIBLE); } }); tools.setPivotX(centerX); final Animator toolsAnimator = ObjectAnimator.ofPropertyValuesHolder(tools, PropertyValuesHolder.ofFloat("alpha", 0f), PropertyValuesHolder.ofFloat("scaleX", 0.8f)); toolsAnimator.setDuration(TOOLS_DURATION); AnimatorSet set = new AnimatorSet(); set.play(toolsContainerAnimator).with(toolsAnimator) .before(fabAnimator1).before(fabAnimator2).before(fabAnimator3); set.start(); } } }); toolsContainer.setVisibility(toggleButton.isChecked() ? View.VISIBLE : View.INVISIBLE); tools.setAlpha(toggleButton.isChecked() ? 1f : 0f); } } アニメーションの長いコードがあるために、ここでやっていること(つまり toolsContainer と fab の visibility を切り替えること)がわかりにくくなっています。
それを解消するために Transition API が使えます(Transition API はもともとそういうための用意されたもののようです)。それは次回に。

↓実行結果



ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する - with Transition API -

$
0
0
ViewAnimationUtils.createCircularReveal() を使って FAB の transforming を実現する
では、直接Activityに複雑なアニメーションを記述しました。それにより、本質的なコード(toolsContainer と fab の visibility の切り替え)がアニメーションのコードに埋もれてしまい、何をやっているのかわかりずらい状況になっていました。
そこで Transition API を使ってアニメーション部分を Activity から引き剥がしました。

完全な実装は
https://github.com/yanzm/FabTransformingSample
にあります。

MainActivityからは Animator オブジェクトが完全になくなり、RecyclerViewやバックキー部分のコードを追加しても前回より短くなっています。 visibility の切り替えなど view のパラメータ値の変更だけになり、何をやっているのかがわかりやすくなりました。 アニメーション部分は FabTransformation というクラスにまとめています。 public class MainActivity extends AppCompatActivity { private static final int HORIZONTAL_FACTOR = 2; private float diff; private ViewGroup sceneRoot; private View toolsContainer; private View tools; private FloatingActionButton fab; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); toolsContainer = findViewById(R.id.tools_container); tools = findViewById(R.id.tools); fab = (FloatingActionButton) findViewById(R.id.fab); sceneRoot = (ViewGroup) findViewById(R.id.scene_root); sceneRoot.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int[] toolsLocation = new int[2]; toolsContainer.getLocationInWindow(toolsLocation); int[] fabLocation = new int[2]; fab.getLocationInWindow(fabLocation); diff = (toolsLocation[1] + toolsContainer.getHeight() / 2) - (fabLocation[1] + fab.getHeight() / 2); final float pivotX = fabLocation[0] + fab.getWidth() / 2 - toolsLocation[0] - diff * HORIZONTAL_FACTOR; toolsContainer.setPivotX(pivotX); tools.setPivotX(pivotX); sceneRoot.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { changeFabMode(true, true); } }); changeFabMode(false, false); // recycler view setup final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setAdapter(new AndroidVersionAdapter()); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState != RecyclerView.SCROLL_STATE_IDLE) { if (fab.getVisibility() != View.VISIBLE) { changeFabMode(false, true); } } } }); } @Override public void onBackPressed() { if (fab.getVisibility() != View.VISIBLE) { changeFabMode(false, true); return; } super.onBackPressed(); } private void changeFabMode(boolean transformed, boolean animate) { if (animate) { final TransitionSet transition = new FabTransformation(transformed, fab.getHeight() / 2f); TransitionManager.beginDelayedTransition(sceneRoot, transition); } final float baseMargin = getResources().getDimension(R.dimen.fab_margin); final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) fab.getLayoutParams(); params.bottomMargin = (int) (baseMargin - (transformed ? diff : 0)); params.setMarginEnd((int) (baseMargin + (transformed ? diff * HORIZONTAL_FACTOR : 0))); fab.setLayoutParams(params); toolsContainer.setVisibility(transformed ? View.VISIBLE : View.INVISIBLE); tools.setVisibility(transformed ? View.VISIBLE : View.INVISIBLE); tools.setScaleX(transformed ? 1f : 0.8f); fab.setVisibility(transformed ? View.INVISIBLE : View.VISIBLE); } } FabTransformationでは複数の Transition を組み合わせて FAB の transforming を実現するアニメーションを構築しています。 ここでは Transition API で用意されている ChangeTransformFadeChangeBoundsに加えて、ViewAnimationUtils.createCircularReveal()を利用する CircularRevealTransition を作って利用しています。





SharedElement をフェードインさせたい

$
0
0
やりたいことは次の動画を見てもらうのが早いです。



言葉にすると、
1. Activity1 から Activity2 に遷移するときに、
2. ある View を sharedElement としてアニメーション(移動)させたい
3. Activity2 では sharedElement が移動している間に表示している内容を変更したい

Activity1 の方のレイアウトは ImageView 一つだけで、これが sharedElement の対象。 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_width="128dp" android:layout_height="128dp" android:layout_gravity="bottom" android:src="@drawable/sample_image1" android:transitionName="image" tools:ignore="ContentDescription"/> </FrameLayout> Activity2 の方は ImageView が2つ重なっていて、一つ目の ImageView には Activity1 と同じ画像、二つ目の ImageView には Transition 後に表示したい画像がセットされている。二つ目の ImageView は非表示(INVISIBLE)。二つの ImageView の container である FrameLayout が sharedElement の対象。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <net.yanzm.sample.SquareFrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:transitionName="image"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/sample_image1" tools:ignore="ContentDescription"/> <ImageView android:id="@+id/image2" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/sample_image2" android:visibility="invisible" tools:ignore="ContentDescription"/> </net.yanzm.sample.SquareFrameLayout> </LinearLayout> Activity1側で ActivityOptionsCompat.makeSceneTransitionAnimation() を使って Activity2 を呼び出すと、画面遷移時に SharedElement が移動します。 当たり前ですがこの段階では Activity2 側の二つ目の ImageView は出てきません(INVISIBLEなので)。 public class TransitionActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_transition); findViewById(R.id.image).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { move(view); } }); } private void move(View view) { Intent intent = new Intent(this, TransitionActivity2.class); final Bundle options = ActivityOptionsCompat .makeSceneTransitionAnimation(this, view, "image") .toBundle(); startActivity(intent, options); } }



そこで、独自の SharedElement 用 TransitionSet を作ります。

デフォルトはプラットフォームの @transition/move が指定されており、中身は次のようになっています。 <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"> <changeBounds/> <changeTransform/> <changeClipBounds/> <changeImageTransform/> </transitionSet> そこで、以下のようなクラスを用意しました。 public class CustomTransitionSet extends TransitionSet { public CustomTransitionSet() { addTransition(new ChangeBounds()); addTransition(new ChangeTransform()); addTransition(new ChangeClipBounds()); addTransition(new ChangeImageTransform()); addTransition(new CustomTransition().addTarget(R.id.image2)); } } デフォルトの設定 + CustomTransition を追加しています。 CustomTransition は Activity2 の二つ目の ImageView だけを対象にしたいので、addTarget で対象を絞っています。
この CustomTransitionSet を Activity2 で SharedElement 用の Transition としてセットします。 public class TransitionActivity2 extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setSharedElementEnterTransition(new CustomTransitionSet()); setContentView(R.layout.activity_transition2); } } CustomTransition では Transition 開始時の view の Visibility を持っておいて、それが VISIBLE 以外だったらフェードアウト、VISIBLE だったらフェードインのアニメーションをするようにしました。 public class CustomTransition extends Transition { // TransitionValues に追加するときのキーは パッケージ名:クラス名:プロパティ名 private static final String PROP_NAME_VISIBILITY = "net.yanzm.sample:CustomTransition:visibility"; @Override public void captureStartValues(TransitionValues transitionValues) { // visibility の値を持っておく final View view = transitionValues.view; transitionValues.values.put(PROP_NAME_VISIBILITY, view.getVisibility()); } @Override public void captureEndValues(TransitionValues transitionValues) { // end の値は使わないので何もしない } @Override public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { if (startValues == null || startValues.view == null) { return null; } final View view = startValues.view; final int visibility = (int) startValues.values.get(PROP_NAME_VISIBILITY); final boolean isEnter = visibility != View.VISIBLE; view.setVisibility(View.VISIBLE); view.setAlpha(isEnter ? 0f : 1f); final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", isEnter ? 1f : 0f); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setAlpha(1f); view.setVisibility(isEnter ? View.VISIBLE : View.INVISIBLE); super.onAnimationEnd(animation); } }); return anim; } } これで一番上に載せた動画のような動作になりました!


BottomNavigationView で画面回転時に位置を保持するようにしてみた

$
0
0
注意:以下の内容は Design Support Library v25.0.1 時点でのものです

v25.0.0 から Design Support Library に BottomNavigationView が追加されましたが、最新版(v25.0.1)でも画面回転時に選択アイテムの位置を保持してくれず、選択が一番最初のアイテムに戻ってしまう問題があります。しかも選択中のアイテムを変更するAPIも現状では用意されていません。

いちを以下の方法で選択アイテムを変更することはできます。 final View view = findViewById(menuId); if (view != null) { view.performClick(); } でももにょるよね...

本家が対応するまでの間、上記の苦し紛れの方法を駆使した CustomBottomNavigationView を用意しました。これで画面回転時も位置が保持されます。

CustomBottomNavigationView

ついでにこれを使って fragment の入れ替えもちゃんと実装したサンプルを用意したので、ぜひ参考にしてください。

https://github.com/yanzm/BottomNavigationSample


本家で早く対応してください。



RecyclerView の SnapHelper を調べてみた

$
0
0
この投稿は GeekWomenJapan Advent Calendar 2016の25日目です。

2016年の11月に droid girlsというAndroidの技術に特化した女性コミュニティを立ち上げました。
第2回 Meetupでは私が講師を担当してRecyclerViewを取り上げたのですが、その時に SnapHelper というものを発見してしまいました。
今日はこの SnapHelper についての話です。

ちなみに第3回 Meetupでは vector drawable を取り上げます。開催は2017年1月下旬を予定しています。


本題

以下の検証は v25.1.0 で行っています。

RecyclerView.OnFlingListener および関連する SnapHelper などは v24.2.0で追加されました。


SnapHelper の abstract メソッド

  • public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)
    • snap 対象の Adapter での位置を返す
  • public abstract View findSnapView(LayoutManager layoutManager)
  • public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView)
    • snap する位置までの距離を返す

PagerSnapHelper

PagerSnapHelper は ViewPagerみたいな挙動を実現するのに使います。そのため、RecyclerView も RecyclerView.Adapter が提供する子 View も height と width が MATCH_PARENT である必要があります。

使用例 public class PagerSnapHelperActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_snap_helper); final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); recyclerView.setAdapter(new MyPagerAdapter()); final PagerSnapHelper pagerSnapHelper = new PagerSnapHelper(); pagerSnapHelper.attachToRecyclerView(recyclerView); } private static class MyPagerAdapter extends RecyclerView.Adapter<ViewHolder> { private static final int[] colors = { Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY }; @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return ViewHolder.create(parent); } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.textView.setText(String.valueOf(position)); holder.textView.setBackgroundColor(colors[position]); } @Override public int getItemCount() { return colors.length; } } private static class ViewHolder extends RecyclerView.ViewHolder { static ViewHolder create(@NonNull ViewGroup parent) { final TextView textView = new TextView(parent.getContext()); textView.setTextSize(32); textView.setGravity(Gravity.CENTER); textView.setLayoutParams(new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return new ViewHolder(textView); } final TextView textView; private ViewHolder(@NonNull TextView itemView) { super(itemView); this.textView = itemView; } } } PagerSnapHelper のコード解説

findSnapView() では、RecyclerView の中心位置と各子 View の中心位置との距離を比較して、一番近い子 View を snap 対象としています。
findTargetSnapPosition() では、上端または左端にある子 View を基準に、fling 時の velocity の正負に応じて隣の子 View の位置を返しています。velocity の絶対値は使っていないので、弱く fling しても強く fling しても隣のページに移動するだけです。
calculateDistanceToFinalSnap() では RecyclerView の中心と snap 対象の View の中心との差を返しています。


LinearSnapHelper

LinearSnapHelper は snap 対象の View の中心が RecyclerView の中心に来るように snap します。

使用例 public class LinearSnapHelperActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_snap_helper); final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new MyPagerAdapter()); final LinearSnapHelper linearSnapHelper = new LinearSnapHelper(); linearSnapHelper.attachToRecyclerView(recyclerView); } private static class MyPagerAdapter extends RecyclerView.Adapter<ViewHolder> { private static final int[] colors = { Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY, Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.LTGRAY }; @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return ViewHolder.create(parent); } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.textView.setText(String.valueOf(position)); holder.textView.setBackgroundColor(colors[position]); } @Override public int getItemCount() { return colors.length; } } private static class ViewHolder extends RecyclerView.ViewHolder { static ViewHolder create(@NonNull ViewGroup parent) { final TextView textView = new TextView(parent.getContext()); textView.setTextSize(32); textView.setGravity(Gravity.CENTER); int height = parent.getMeasuredHeight() / 4; textView.setLayoutParams(new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height)); return new ViewHolder(textView); } final TextView textView; private ViewHolder(@NonNull TextView itemView) { super(itemView); this.textView = itemView; } } } LinearSnapHelper のコード解説

findSnapView() では、RecyclerView の中心位置と各子 View の中心位置との距離を比較して、一番近い子 View を snap 対象としています。
findTargetSnapPosition() では、velocity の大きさから対応するスクロール量を計算し、1子ビューあたりの大きさからスクロールで移動する子ビューの数を計算し、現在の位置から移動する子ビューの数だけ離れた位置を返しています。
calculateDistanceToFinalSnap() では RecyclerView の中心と snap 対象の View の中心との差を返しています。


上端に snap する SnapHelper

LinearSnapHelper が中心に snap するので、そのコードを参考に center を計算する部分を top に変えれば、上端(start)に snap するようにできます。 public class MyLinearSnapHelper extends SnapHelper { ... @Override public int[] calculateDistanceToFinalSnap( @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { int[] out = new int[2]; if (layoutManager.canScrollHorizontally()) { out[0] = distanceToTop(layoutManager, targetView, getHorizontalHelper(layoutManager)); } else { out[0] = 0; } if (layoutManager.canScrollVertically()) { out[1] = distanceToTop(layoutManager, targetView, getVerticalHelper(layoutManager)); } else { out[1] = 0; } return out; } ... @Override public View findSnapView(RecyclerView.LayoutManager layoutManager) { if (layoutManager.canScrollVertically()) { return findTopView(layoutManager, getVerticalHelper(layoutManager)); } else if (layoutManager.canScrollHorizontally()) { return findTopView(layoutManager, getHorizontalHelper(layoutManager)); } return null; } private int distanceToTop(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView, OrientationHelper helper) { final int childTop = helper.getDecoratedStart(targetView); final int containerTop; if (layoutManager.getClipToPadding()) { containerTop = helper.getStartAfterPadding(); } else { containerTop = 0; } return childTop - containerTop; } ... @Nullable private View findTopView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; final int top; if (layoutManager.getClipToPadding()) { top = helper.getStartAfterPadding(); } else { top = 0; } int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childTop = helper.getDecoratedStart(child); int absDistance = Math.abs(childTop - top); /** if child top is closer than previous closest, set it as closest **/ if (absDistance < absClosest) { absClosest = absDistance; closestChild = child; } } return closestChild; } ... }


v7 Preference Support Library を Material Design にする

$
0
0
フレームワーク

v7 Preference Support Librarycom.android.support:preference-v7:25.1.0

v14 Preference Support Librarycom.android.support:preference-v14:25.1.0

v7 Preference Support Library 例

public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingsFragment()) .commit(); } public static class SettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref, rootKey); } } }

Material Design 化 例

<?xml version="1.0" encoding="utf-8"?> <resources> <style name="Theme.Setting"> <item name="android:listSeparatorTextViewStyle">@style/Preference.ListSeparator</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay</item> </style> <style name="Preference.ListSeparator" parent="android:Widget.TextView"> <item name="android:minHeight">48dp</item> <item name="android:gravity">center_vertical</item> <item name="android:textAppearance">@style/TextAppearance.AppCompat.Body2</item> <item name="android:textColor">?colorAccent</item> <item name="android:maxLines">1</item> <item name="android:paddingLeft">16dp</item> <item name="android:paddingRight">16dp</item> </style> </resources> values/styles_preference.xml <?xml version="1.0" encoding="utf-8"?> <resources> <style name="Preference"> <item name="android:layout">@layout/preference_material</item> </style> <style name="Preference.DropDown"> <item name="android:layout">@layout/preference_dropdown_material</item> </style> <style name="Preference.SeekBarPreference"> <item name="android:layout">@layout/preference_widget_seekbar_material</item> <item name="adjustable">true</item> <item name="showSeekBarValue">true</item> </style> <style name="PreferenceFragmentList"> <item name="android:paddingLeft">0dp</item> <item name="android:paddingRight">0dp</item> </style> </resources> values-v17/styles_preference.xml <?xml version="1.0" encoding="utf-8"?> <resources> <style name="PreferenceFragmentList"> <item name="android:paddingStart">0dp</item> <item name="android:paddingEnd">0dp</item> </style> </resources> layout/preference_material.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" android:baselineAligned="false" android:focusable="true" android:gravity="center_vertical" android:minHeight="72dp" android:paddingLeft="8dp" android:paddingRight="8dp"> <FrameLayout android:id="@+id/icon_frame" android:layout_width="56dp" android:layout_height="wrap_content"> <android.support.v7.internal.widget.PreferenceImageView android:id="@android:id/icon" android:layout_width="40dp" android:layout_height="40dp" android:layout_gravity="center_horizontal"/> </FrameLayout> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_weight="1" android:orientation="vertical" android:paddingBottom="20dp" android:paddingTop="20dp"> <TextView android:id="@android:id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="marquee" android:fadingEdge="horizontal" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:textColor="?android:attr/textColorPrimary" tools:text="title"/> <TextView android:id="@android:id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:maxLines="4" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textColor="?android:attr/textColorSecondary" tools:text="summary"/> </LinearLayout> <!-- Preference should place its actual preference widget here. --> <LinearLayout android:id="@android:id/widget_frame" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:gravity="center_vertical" android:orientation="vertical"/> </LinearLayout> layout/preference_dropdown_material.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" android:focusable="true" android:gravity="center_vertical" android:minHeight="72dp" android:paddingLeft="8dp" android:paddingRight="8dp"> <Spinner android:id="@+id/spinner" android:layout_width="0dp" android:layout_height="wrap_content" android:visibility="invisible"/> <FrameLayout android:id="@+id/icon_frame" android:layout_width="56dp" android:layout_height="wrap_content"> <android.support.v7.internal.widget.PreferenceImageView android:id="@android:id/icon" android:layout_width="40dp" android:layout_height="40dp" android:layout_gravity="center_horizontal"/> </FrameLayout> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_weight="1" android:orientation="vertical" android:paddingBottom="20dp" android:paddingTop="20dp"> <TextView android:id="@android:id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="marquee" android:fadingEdge="horizontal" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:textColor="?android:attr/textColorPrimary" tools:text="title"/> <TextView android:id="@android:id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:maxLines="4" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textColor="?android:attr/textColorSecondary" tools:text="summary"/> </LinearLayout> <!-- Preference should place its actual preference widget here. --> <LinearLayout android:id="@android:id/widget_frame" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:gravity="center_vertical" android:orientation="vertical"/> </LinearLayout> layout/preference_widget_seekbar_material.xml <?xml version="1.0" encoding="utf-8"?> <!-- Layout used by SeekBarPreference for the seekbar widget style. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" android:clipToPadding="false" android:gravity="center_vertical" android:minHeight="72dp" android:paddingLeft="8dp" android:paddingRight="8dp"> <ImageView android:id="@+android:id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:minWidth="@dimen/preference_icon_minWidth"/> <RelativeLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_weight="1" android:clipChildren="false" android:clipToPadding="false" android:paddingBottom="20dp" android:paddingTop="20dp"> <TextView android:id="@+android:id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="marquee" android:fadingEdge="horizontal" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:textColor="?android:attr/textColorPrimary" tools:text="title"/> <TextView android:id="@android:id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@android:id/title" android:layout_alignStart="@android:id/title" android:layout_below="@android:id/title" android:layout_marginTop="2dp" android:maxLines="4" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textColor="?android:attr/textColorSecondary" tools:text="summary"/> <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation to the children of this container layout. Otherwise, the animated pressed state will also play for the thumb in the AbsSeekBar in addition to the preference's ripple background. The background of the SeekBar is also set to null to disable the ripple background --> <android.support.v7.preference.UnPressableLinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignLeft="@android:id/title" android:layout_alignStart="@android:id/title" android:layout_below="@android:id/summary" android:layout_marginTop="2dp" android:clipChildren="false" android:clipToPadding="false"> <SeekBar android:id="@+id/seekbar" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@null" android:clickable="false" android:focusable="false" android:paddingEnd="@dimen/preference_seekbar_padding_end" android:paddingLeft="@dimen/preference_seekbar_padding_start" android:paddingRight="@dimen/preference_seekbar_padding_end" android:paddingStart="@dimen/preference_seekbar_padding_start"/> <TextView android:id="@+id/seekbar_value" android:layout_width="@dimen/preference_seekbar_value_width" android:layout_height="match_parent" android:ellipsize="marquee" android:fadingEdge="horizontal" android:fontFamily="sans-serif-condensed" android:gravity="center" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/> </android.support.v7.preference.UnPressableLinearLayout> </RelativeLayout> </LinearLayout>

Support library 25.1.0 で OnBackStackChangedListener の挙動が変わっていた

$
0
0
追記: バグでした。 https://code.google.com/p/android/issues/detail?id=230353

FragmentTransaction.addToBackStack()を使って Fragment をバックスタックに移動すると、バックスタックの状態が変わるので FragmentManager.OnBackStackChangedListenerの onBackStackChanged() が呼ばれます。

次のコードを見てください。 public class MainActivity extends AppCompatActivity { private static final String TAG = "BackStackSample"; private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() { @Override public void onBackStackChanged() { final FragmentManager manager = getSupportFragmentManager(); final int count = manager.getBackStackEntryCount(); Log.d(TAG, "onBackStackChanged : " + count); Log.d(TAG, "onBackStackChanged : " + manager.findFragmentById(R.id.container)); if (count > 0) { Log.d(TAG, "onBackStackChanged : " + manager.getBackStackEntryAt(count - 1)); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final FragmentManager manager = getSupportFragmentManager(); manager.addOnBackStackChangedListener(backStackChangedListener); findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { manager.beginTransaction() .add(R.id.container, new FragmentA()) .addToBackStack(String.valueOf(System.currentTimeMillis())) .commit(); } }); findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { manager.beginTransaction() .replace(R.id.container, new FragmentB()) .addToBackStack(String.valueOf(System.currentTimeMillis())) .commit(); } }); } @Override protected void onDestroy() { getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener); super.onDestroy(); } }


このコードを実行し、add ボタンをタップし、その次に replace ボタンをタップし、最後にバックキーをタップすると、次のようなログが出力されます。

25.0.1D/BackStackSample: onBackStackChanged : 1 D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057} D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683} D/BackStackSample: onBackStackChanged : 2 D/BackStackSample: onBackStackChanged : FragmentB{d8c00cf #1 id=0x7f0b0057} D/BackStackSample: onBackStackChanged : BackStackEntry{bf59a5c #1 1487898292435} D/BackStackSample: onBackStackChanged : 1 D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057} D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683}

25.1.0D/BackStackSample: onBackStackChanged : 1 D/BackStackSample: onBackStackChanged : null D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648} D/BackStackSample: onBackStackChanged : 1 D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059} D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648} D/BackStackSample: onBackStackChanged : 2 D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059} D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305} D/BackStackSample: onBackStackChanged : 2 D/BackStackSample: onBackStackChanged : FragmentB{bf59a5c #1 id=0x7f0b0059} D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305} D/BackStackSample: onBackStackChanged : 1 D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059} D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}

なんてこったい。25.0.1 までは add() と replace() でそれぞれ1回しか onBackStackChanged() が呼ばれていなかったのに、25.1.0 からそれぞれ2回呼ばれるようになっているじゃあないですか。
しかも、add() または replace() 先の view id のついた Fragment がどれになっているかを見ると、2回呼ばれるうちの最初の方は Transaction が実行される前のようです。

一方、バックキーで pop するときは 25.0.1 と 25.1.0 で挙動は同じです。

バグ...のような気がしなくもないけれど 25.2.0 でも変わらず2回呼ばれます。


ActionBar のタイトルを foreground の Fragment に応じて変えるなどの処理を onBackStackChanged() 内でやっていたのですが、view id のついた Fragment が BackStack に入る方を指している状態で呼ばれると困るわけです。

ちなみに onBackStackChanged() をトリガーとする理由は、onAttachFragment() だとバックキーで pop されたときに呼ばれないからです。

妥当な対処方法としては
  • 1. add() or replace() のときは onAttachFragment() で処理し、onBackStackChanged() に pop されたかどうかの判定を入れて pop された時だけ処理する
  • 2. add() or replace() のときはその場処理し、onBackStackChanged() に pop されたかどうかの判定を入れて pop された時だけ処理する
あたりかと。

ちなみに 1 でやるとこんな感じです。 public class MainActivity extends AppCompatActivity { private static final String TAG = "BackStackSample"; private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() { private int backStackCount; @Override public void onBackStackChanged() { final FragmentManager manager = getSupportFragmentManager(); final int count = manager.getBackStackEntryCount(); if (backStackCount > count) { // pop された final Fragment fragment = manager.findFragmentById(R.id.container); if (fragment != null) { onCurrentFragmentChanged(fragment); } } backStackCount = count; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final FragmentManager manager = getSupportFragmentManager(); manager.addOnBackStackChangedListener(backStackChangedListener); findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { manager.beginTransaction() .add(R.id.container, new FragmentA()) .addToBackStack(String.valueOf(System.currentTimeMillis())) .commit(); } }); findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { manager.beginTransaction() .replace(R.id.container, new FragmentB()) .addToBackStack(String.valueOf(System.currentTimeMillis())) .commit(); } }); } @Override protected void onDestroy() { getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener); super.onDestroy(); } @Override public void onAttachFragment(Fragment fragment) { super.onAttachFragment(fragment); onCurrentFragmentChanged(fragment); } private void onCurrentFragmentChanged(Fragment fragment) { // ここで fragment に応じて ActionBar のタイトルを変えたりする Log.d(TAG, "onCurrentFragmentChanged : " + fragment); } }

onBackStackChanged() だけで対処できていたのに、つらい...

DroidKaigi 2017 で登壇します。

$
0
0
DroidKaigi 2017

2017年3月9日(木) 10:40 - 11:30
Room 3

です。

ドメイン駆動設計とAndroidアプリ開発についての話をします。
ドメイン駆動設計まったく知らないという人向けです。

かつてなく気合を入れて準備しております。
朝早いですが、ぜひ聴きに来てください。


DroidKaigi 2017 で登壇しました。

$
0
0
スライドだけ公開しても伝わらないと思うので、スピーチ原稿と合わせて公開します。
すごく長いです。


本日はドメイン駆動設計というものについて、Androidアプリ開発と絡めてお話させていただきます。
私あんざいゆきと言います。Androidアプリの開発をなりわいとしております。
長らく Y.A.Mの雑記帳というブログでAndroidの技術情報を発信しています。最近はなかなか投稿できなくなってしまいましたが、それも仕事としてAndroidに関われているためです。Androidを触り始めたころはまだ学生だったので時間があったんでしょうね。
はじめて Android に関するエントリを投稿したのは 2009 年 5 月 24 日です。当時はJavaFXを触っていたので、NetbeansでAndroidをやろうとしていたようです。
次の日にはEclipseに乗り換えるんですけどね。やっぱりAndroidアプリを開発するならEclipseですよねー。
当時のAndroidのバージョンは1.5、Fragment もなく、Support Library もなく、マルチタッチすらなく、ストアは Google Play ではなく Android Market という名前でした。
ここから2、3年くらいは、仕事でAndroid アプリを開発している人はもっぱらメーカーのプリインアプリを作っている方たちで、多くの人は趣味でAndroidアプリを開発して、マーケットに公開していました。
彼ら、彼女らが作るアプリはほとんどが無料で、ユーザーは開発者自身、自分が欲しいもの、自分にとって便利なものをみな作っていました。
こういったものは大抵がシンプルな単機能のもので、継続的にメンテナンスすることを考えずに作られています。
当時趣味でアプリを作っていた人で、半年後や1年後に自分のコードをみて、何をしているのかわからない、という状況になった方はいらっしゃるのではないでしょうか。私もその一人です。
さらに趣味アプリの場合、コードをいじるのは自分だけです。今ほど github を使っている人もいませんでしたし、趣味アプリの場合、OSS として github 上に公開してもコミットするのは自分だけという状況は多いのでないでしょうか。
このように規模が小さくシンプルなアプリで、作って公開したら終わり、しかも作り手は自分だけ、という状況では設計手法なんて考えなくてよかったのです。設計にそれほど注意を払わなくても作成できますから。
さて、その後 Android は破竹の勢いで世界を席巻しました。スマートフォンも普及し、今ではあるのが当たり前、対応するのが当たり前のプラットフォームです。
スマートフォンを対象に新たにサービスを立ち上げたり、すでに展開しているビジネスのクライアントとしてアプリを開発するというのは、趣味アプリとは前提が異なります。
まず、サービスとしてさまざまな機能を提供するため、アプリが複雑になります。画面数も多く、さまざまなことに注意を払う必要があります。
次に作って終わりではありません。継続的にメンテナンスし、ビジネス要件の変化に対応し、機能の追加や変更が必要です。ランタイムパーミッションに対応するなど、プラットフォームの進化にも追随しなくてはいけません。
ライバルのアプリに新しい機能が追加されたら、うちのアプリにも入れろと言われ、あれやこれやの施策をやりたいと言われ、しかも早くやれと言われる。開発するほうは大変です。
さらに、作り手は自分だけではありません。複数の開発者がアプリのさまざまな部分に手を入れます。人が入れ替わることもあるでしょう。
他の人が書いたところをすべて読まなくては機能変更ができない、という状況では困ります。
我々のアプリは危機に瀕している。
機能を満たすだけのコードが無秩序に積み上がった状態はまるでハウルの動く城。
あやまってネジを抜いてしまったら、全てが瓦解しかねない。
デグレの嵐、燃えるユーザーレビュー。
おそろしい。
よくわからないコードをいじる時はおそるおそる。すごく時間がかかる...
このままではいけない。

どうすれば、複数人で複雑なアプリを継続的に、安定して素早く開発していけるのか。

なんとかアーキテクチャや、なんたらパターンがあふれているけれど、どれもしっくりこない。 うまくいっているという話もあまり聞こえてこない。 もし技術的なパターンを適用するだけでうまくいくなら、今頃みんなそれをやっているはず。でも、そんなふうにはなっていない。 表面的な技術的パターンではなく、もっと本質的なことを考えるべきではないのか。

そのヒントを探してドメイン駆動設計の本を手に取りました。
最初に読んだのが「エリック・エヴァンスのドメイン駆動設計」です。
ドメイン駆動設計の本といえばこれですね。
すごく分厚いです。3cmあります。
一人で読み切るのは大変だと思います。私は友人と読書会をすることで通読できました。

チームでDDDに取り組もうとするときには、少なくとも一人はこの本を読み切った人がいるべきだと思っています。 実は、この本は読みながら出てきた内容を順番にチームで取り組んでいけば良い、という構成ではありません。最後の方、第4節、14章に大事なことが書いてあるんです。
次に読んだのが「実践ドメイン駆動設計」です。
こちらのほうが説明の日本語がわかりやすいかと思います。

この本は実践とついているだけあって、チームでDDDに取り組むにはどうやっていけばよいかという視点から書かれています。

先ほど言及したエリック・エヴァンスの本の14章で扱っている内容は、2章、3章で取り上げられています。

「実践ドメイン駆動設計」のほうが、チームでDDDを学びながら順次取り組んでいきたい、という要求に的しています。
ただし、この本ではエリック・エヴァンスの本を読んでいる前提で詳細な解説がされていないところもありますから、結局は2冊とも読むべきだと思います。

「ドメイン駆動設計をどうAndroidアプリ開発に取り入れるか」が、このセッションの主題ですが、その話をするにはまずドメイン駆動設計とはなんなのかの話をしなければいけません。
ドメイン駆動設計、Domain Driven Design、という言葉を聞いて最初に思ったことは「ドメイン」とはいったいなんだろう?でした。 インターネット上の住所などと言われるドメイン名のドメインのことではありません。

本には次のように書かれています。
私たちが作るアプリにもユーザーがそれを適用する対象領域があります。
例えば料理のレシピを検索するアプリはどうでしょうか。
ユーザーはこのアプリをどのような領域に適用するのか。つまりこのアプリで何をするのか。アパートを検索するわけではありませんね。料理のレシピを検索します。つまりこのアプリのドメインは料理のレシピ、または料理そのものと言えます。
アパートを検索するなら、別のアプリを使いますね。ユーザーは建物が欲しいわけではなく、住むところを探したいわけですから、このようなアプリのドメインはアパートではなく、住まいでしょう。住むということとも言えそうです。
UberやLyftはどうでしょうか。適用する対象領域は「移動」と言えるでしょう。海を渡るためには使いませんからドメインは「近距離移動」でしょうか。

自分のアプリやサービスのドメインについて考えてみてください。

ドメインについて考えると、そこには実際に存在するものから概念的なものまでさまざまなものが含まれます。例えば、食材や調理器具や間取りや車など。
githubのようなソースコードを管理するサービスのドメインにはソフトウェアという概念が含まれるでしょう。

ドメイン駆動設計でのドメインとは何かがわかってきました。
すると次の疑問は「ドメイン駆動で設計するとはどういうことか?何をするのか?」です。
まずドメインがあります。
次にドメインエキスパートという人が登場します。
また知らない単語が出てきました。ドメインエキスパートってなんでしょう?
端的にいうとドメインに詳しい人です。ドメインが何かは先ほど話しましたね。アプリによってドメインは異なりますから、アプリによってドメインエキスパートも異なります。
エキスパートとついているので専門家じゃないといけないと思うかもしれませんが、そうではありません。ドメインについて自分より詳しい人はみなドメインエキスパートです。
ユーザーだったり、同僚だったり、さまざまです。
ドメインエキスパートが何かわかったので、ドメイン駆動設計で何をするのかに戻りましょう。
ドメインエキスパートの頭の中には、ドメインを構成する物や振る舞い、関係性などの概念的な何かがあります。

例えば料理をするとはどういうことですかと聞くと、「材料を用意して、それぞれの分量を計り、手順に沿って調理していく」ことです。のような答えが出てくるでしょう。

なにやら「材料」というものがあって、それが複数必要なようだ。材料には「分量」というものがあって、「分量」というのは測るものらしい。さらに「手順」というものがあって「調理」というものをするらしい、と。
頭の中は直接見えませんから、ドメインエキスパートに質問したり話を聞いたり互いに協力して、ドメインエキスパートの頭の中にある概念的な何かをうまく取り出し、解釈し、蒸留し、我々のソフトウェアに役立つモデルとして作り上げます。
そうして作り上げたものがドメインを反映したモデル、ドメインモデルと呼んでいるものです。

ドメインモデルは図を書いて説明したり、文章で説明したりできますが、図それ自体がドメインモデルである、というわけではありません。 あくまで頭の中の共通概念としてモデル化したものです。
ドメインエキスパートと話をしていると、ドメインを構成する言葉が見つかります。
先ほどの例だと「材料」や「分量」や「調理」という単語や「材料を用意する」「分量を計る」などのフレーズです。ドメイン駆動設計ではこれらドメインを構成する言葉をユビキタス言語といいます。
ユビキタスは、日本語で「どこにでも存在する」というような意味ですが、ドメイン駆動設計ではユビキタス言語として見つけ出した単語やフレーズをあらゆる場所で使います。ドキュメントはもちろん、会議での会話、ドメインモデル、そしてそれを実装するコードにも使います。

ドメインエキスパートを含めたチーム全員が同じユビキタス言語を使います。

そのため、何をユビキタス言語とするかはドメインエキスパートを含めたチーム全員で議論して合意の上で決めます。
ユビキタス言語はドメインエキスパートと開発者、そしてチームの共通言語です。単なる用語集ではありません。言葉は進化し、育っていくものです。


「開発者とドメインエキスパートが協力して、ドメインを構成するユビキタス言語を確立し、それを使ってドメインを反映したモデルを作り上げる」というところまできました。
ドメインモデルを作ったら、それを正確にコードで表現するように実装します。

ドメイン駆動設計の利点としてよく、コードが設計であり、設計がコードであるという点が挙げられますが、コードがドメインモデルを正確に表現していれば、それはすなわち設計である、ということです。
共通の概念モデルとしてドメインモデルを作り、それをコードで正確に表現するよう実装するので、他の人にとっても誤解しにくいコードになります。

じゃあ最初にドメインモデルを全部作り上げ、それから一気に実装すればいいのかというと、そうではありません。
最初から完璧なドメインモデルを作ることは不可能です。実装してみると、このモデルだとうまく実装できない、ということが判明したり、実装の途中でもっとよいモデルを思いつくことはよくあります。


そのためドメイン駆動設計では、ドメインモデルを作り、実装してみて、その結果をフィードバックし、ドメインモデルを修正したり変更したり、まったく新しいモデルを作ったり、そしてそれをまた実装する。これを繰り返してドメインモデルとコードの両方を洗練させていきます。
つまり、アジャイル的なプロセスを前提としています。
まとめると、

まず、ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけます。
次にユビキタス言語を使ってドメインを適切に反映した、我々のソフトウェアに役立つドメインモデルを作ります。
そして、作ったドメインモデルを正確に表現するようコードを実装し、これを繰り返します。

ドメインモデルを作ってから実装です。いきなり実装ではありません。
やることはわかった。でも実際やるのは難しい。

そこで、ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に2つに分けることができます。
戦略的設計と戦術的設計です。
ドメインモデルを作り上げるために役立つ手法が戦略的設計
ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
今までの話にでてきたユビキタス言語は戦略的設計です。他にも境界づけられたコンテキストやコンテキストマップがあります。

一方の戦術的設計には値オブジェクトやエンティティ、サービスなどがあります。名前を聞いたことがあるという方もいるでしょう。他にも集約やドメインイベントやリポジトリなどもあります。

このようなコーディングにおける技術的な手法は理解しやすいため、ここだけを取り入れてみました、という話がよくありますが、これは完全なDDDではありません。
なぜか。ドメインモデルがないからです。
さて、ドメイン駆動設計が何であるかの話をしてきました。

ここでちょっとドメイン駆動設計が何でないかの話をしたいと思います。
「DDDってClean Architecture のことでしょ?」

違います。Clean Architecture を知らないという人は気にしなくてよいです。
もちろんDDDとClean Architectureを組み合わせて使うということは可能ですし、Clean Architecture のコンセプトはDDDの影響を受けている、参考にしているということはあるでしょう。しかし、ここで言いたいのは DDD = Clearn Architecture ではない、ということです。
「DDDって MVC とか MVP とかの仲間でしょ?」

これも違います。今までの話の中でビューはでてきましたか?出てきてませんね。我々が話してきたのはドメインモデルについてだけです。
「レイヤ化アーキテクチャにすればDDDだよね?」

エリック・エヴァンスの本でレイヤ化アーキテクチャが紹介されているからか、DDD=レイヤ化アーキテクチャにすること、のように勘違いしているのを見かけることがあります。ドメイン駆動設計とアーキテクチャの関係はこのあと取り上げますが、ドメイン駆動設計は特定のアーキテクチャに依存しているものではありません。
「ドメインモデルは作ってないけど技術的なパターンを真似したからDDDだよね?」

ドメイン駆動設計ではドメインモデルを作ってそれを正確に表現するようにコードを実装することであって、特定の技術的パターンをとることではありません。
「ドメインモデルを作ってそれを正確に表現するように実装したからDDDだよね?」

これが正解です。

簡単に言うけど、やるのは難しいんだよね。
そろそろAndroidの話をしましょうか。

Androidアプリ開発でドメイン駆動設計に取り組む場合も同じです。ドメインエキスパートの話を聞いて、概念を適切に反映するドメインモデルを作って、それを正確に表現するよう実装する、これを繰り返します。

そうは言われてもどこから手をつけたらいいかわからない。
アプリにはいろいろ機能があるし、どれをやるべきなの?
どこから手をつけるべきか、それを知るにはアプリの全体像、地勢を把握する必要があります。
そのためにドメイン駆動設計で登場するのが境界づけられたコンテキストとコンテキストマップです。

また知らない単語がでてきました。
境界づけられたコンテキストとコンテキストマップ。

先ほど戦略的設計で名前が出てきました。この2つもドメイン駆動設計の重要な要素です。
境界づけられたコンテキスト。

境界はわかります。コンテキストとは何でしょうか。日本語ではよく文脈などと訳されますね。Androidでよく出てくるあのコンテキストではありません。

ユビキタス言語の言葉が特定の意味を持つ領域がコンテキストです。
例えばAccountという言葉があります。
この言葉がユーザー認証の文脈で語られていた場合、その意味はサービスを利用する際の利用単位のことだとわかります。
一方、銀行の文脈で語られていた場合、その意味は口座になります。
さらに、文学のもとではAccountの意味は報告書になります。
コンテキストが異なると、同じ言葉でも意味が変わります。
ユビキタス言語を構成する言葉が特定の意味を持つ領域がコンテキストであり、
境界づけられたコンテキストの内部では、ユビキタス言語を構成する言葉は特定の意味を持ちます。
ドメインモデルはユビキタス言語で構成されますから、ドメインモデルはそれを構成するユビキタス言語の境界づけられたコンテキストに属します。
では、自分のアプリの境界づけられたコンテキストをどう見つければいいのでしょうか
言語の境界がコンテキストの境界ですから、言葉の境界を探せばよさそうです。
どういうところが言葉の境界になるのでしょうか。
例えばチームが異なると、単一のユビキタス言語を維持するのは難しいでしょう。コミュニケーションにコストがかかることから、チーム内だけの言葉が発展していき、お互いの言葉が徐々に解離していきます。
いずれ、ドメインモデルを境界内で厳密に一貫性のあるものに保つことができなくなります。
特定の機能が外部のライブラリやSDKとして提供されている場合、その部分は別のコンテキストになっていることが多いです。ドメインモデルが一貫性をもつ範囲を考えると理解しやすいでしょう。
同じようにコードベースが異なる場合もヒントになります。わかりやすくいうとgithubのリポジトリが異なるのなら別のコンテキストではないか、ということです。
見つかったコンテキストには名前をつけます。
そして、その名前をユビキタス言語の一部にします。
境界づけられたコンテキストがわかってきたら、次にコンテキストマップを描きます。
コンテキストマップは、現時点の境界づけられたコンテキストと、それらがどのようにやりとりしているのかを示すものです。
大事なのは理想の姿ではなく現状の状態を描くことです。

だからといって正確に把握したものでなければならないというわけでもありません。最初はわかっている範囲で十分です。状態が変わったり、新たな知見が見つかったらその都度マップを更新しましょう。

凝ったつくりにする必要もありません。ホワイトボードに書いたものを写真で撮れば十分です。

依存する他のプロジェクトに何があって、それとどのような関係なのかをチームで共有し、考えるきっかけにします。
作ったマップはいつでも見れるところに置いておく。Activityのライフサイクルポスターの横に貼り出すなんて最高ですね。

境界づけられたコンテキストの間の関係がどのようになるのか、この関係性についてDDDには組織的なパターンや統合のパターンがいくつか紹介されています。
自分たちのコンテキスト間の関係性がこれらのパターンのどれに一番近いか考えてみましょう。
そしてコンテキストマップのコンテキスト同士をつなぐ線にどのパターンなのかを書いてみましょう。

パターンに名前がついていることは、とても重要です。後からチームに参加したひとでもコンテキストマップをみることで、どの依存プロジェクトが協力的で、どこが融通がきかないのか把握することができます。

いくつかのパターンをAndroidアプリ開発でありそうな状況にあてはめてみます。
パートナーシップというのは、成功・失敗の運命を共にする関係です。例えばアプリの主要な機能がすべてそのアプリ用のSDKとして提供されている場合、これらの運命は一蓮托生であり、パートナーシップの関係が一番近いでしょう。
特定の機能を社内SDKとして提供している場合はどうでしょうか。同じ社内ですから、アプリ側のニーズに対応してくれるかもしれない。もし対応してくれるような関係性であるなら、それは顧客/供給者の関係が一番近いでしょう。
3rd party が提供しているSDKではどうでしょうか。例えば twitter や facebook SDK など。この場合我々は提供されているSDKをそのまま利用するしかありません。このような関係は順応者になります。 社内SDKであっても、アプリ側のニーズに対応してくれない関係性なら順応者になります。
公開ホストサービスについてドメイン駆動設計の本には次のように書かれています「サブシステムにアクセスできるようにするプロトコルを、サービスの集合として定義すること。そのプロトコルを公開し、サブシステムと統合する必要のある人が全員使用できるようにすること。」

よくわからないですね。ようはこういうことです。

あるサブシステムとやりとりしたい人が複数います。
それぞれに個別に対応するのは大変なので外部に方法を公開するよということです。方法はRESTかもしれないしRPCかもしれない。
公開ホストサービスと一緒に使われることが多いのが公表された言語です。
わかりやすく言うと、XMLとかJSONとかProtocol Buffer とか、ようは形式が公表されている言語です。
さらに一緒に取り入れることが多いのが腐敗防止層です。
別のコンテキストのモデルによって自分のドメインモデルが汚染されないように、必要に応じて自分のコンテキスト内のモデルに変換します。なので、この変換はコンテキストの外側にあることになります。
コンテキストマップに描くとこのようになります。
PHS は公開ホストサービス、PL は公表された言語、ACL は腐敗防止層です。

公開ホストサービスに限らないのですが、サーバー上のサービスが提供しているコンテキストとアプリのコンテキストが異なるのであれば、モデルの変換が必要になります。
実践ドメイン駆動設計では次のような例があります。本ではXMLですが、ここではJSONに置き換えました。

別のコンテキストの userInRole をそのまま利用側のコンテキスト内で使うのではなく、利用側のコンテキストに存在するドメインモデルである Moderator に変換して利用します。
さて、我々のアプリのドメインが何か考えました。
ユビキタス言語がDDDを行っていくうえで重要な要素であることを理解しました。
境界づけられたコンテキストを見つけ、コンテキストマップを描きました。

次に考えることはなんでしょうか?
実践ドメイン駆動設計では、コンテキストマップの次の章はアーキテクチャです。
ドメイン駆動設計でアーキテクチャというと、レイヤ化アーキテクチャのことがよく出てきます。エリック・エヴァンスの本で紹介されているからか、ドメイン駆動設計ではレイヤ化アーキテクチャを使わなければならない、とか、ドメイン駆動設計はレイヤ化アーキテクチャにすることだ、のような誤った認識をときどき見かけます。
ドメイン駆動設計は特定のアーキテクチャに依存するものではありません。
ではレイヤ化アーキテクチャを取り上げた理由はなぜか、何をしたいのか、
それはドメイン層を隔離することです。
ここでいうドメイン層というのは、ドメインモデルの集まり。正確にいうとドメインモデルを表現したコード、実装の集まりです。
ドメインにある概念や知識、ビジネスロジックとも表現しますが、これをドメインモデルとしてその他から隔離するということです。
Androidアプリ開発において、本来ドメインモデルとして隔離すべき、ドメインにある概念や知識、ビジネスロジックが混入しがちなのが、ユーザーインタフェースです。

なぜ我々はユーザーインタフェースに、ドメインにある概念や知識、ビジネスロジックを詰め込んでしまうのか。
それはアプリの作り方に深く関わっています。
エリック・エヴァンスの本にも登場する利口なUI(スマートUI)は、ユーザーインタフェースにすべてのビジネスロジックを埋め込むパターンです。

その利点には次のように書かれています。
・単純なアプリケーションの場合、生産性が高く、すぐに作れる。
・それほど有能でない開発者でも、この方法なら、ほとんど訓練しないで仕事ができる。
・要求分析が不足していても、プロトタイプをユーザに公開し、その要望を満たすように製品を変更することで、問題を克服できる。
などなど

この利点が利点として生きる場所があります。モックやプロトタイピングです。
モックやプロトタイピングでは、画面のデザインを動くもので素早く確認するのが目的ですから、単純なビジネスロジックも含めすべてユーザーインタフェースに入れます。


つまり、動くアプリをすばやく作ってユーザーに見せて検証したい、という場合、利口なUIになりがち、ということです。


他にも、仕様が決まった後に画面デザインとちょっとした機能説明が書かれたドキュメントが来て、それをもとに実装する...
ありがちな状況ですが、画面デザインと単純な機能を短い期間で作ることを求められる状態でも利口なUIになりがちです。
そこから抜け出すにはどうしたらいいのか。

理想を言えば、機能について議論するところから開発者も参加して、みんなの頭のなかにある概念的なモデルについて観察し、ドメインモデルを作りたい。

でもいきなりそんなこと言われてもできないよ、ってなりますよね。


重要なのは利口なUIになりがちだと認識すること。そして、ドメインにある概念や知識、ビジネスロジックがUIに存在していないか観察し、ドメインモデルとしてUIから隔離できないか考えることです。

ただし注意してほしいのは、ユーザーインタフェースからビジネスロジックを単純に隠蔽することと、ドメインモデルとして隔離することは違います。
Activityとライフサイクルを同期するようにした、なんたらプレゼンターみたいな名前のクラスを作って、そこに処理を全部移譲することではありません。

大事なのはドメインモデルです。ドメインを反映したドメインモデルを作ることで、ユーザーインターフェースからビジネスロジックを引きはがせないか考えましょう。


ここまでの話を一旦まとめましょう。
まず、アプリのドメインとは何か考えました。
次にドメインエキスパートと会話をしてドメインを構成する言語を見つけ、ユビキタス言語として確立し、育てていく必要があることがわかりました。
そして、言語の境界がコンテキストの境界になり、アプリにはそれを構成する境界づけられたコンテキストが複数存在することがありえるということをみました。
コンテキスト同士の関係をコンテキストマップとして描くことで、どのような関係性がコンテキスト同士の間にあるのか把握できるようになりました。
これら戦略的設計と分類できる手法の目的は
ドメインを反映したモデル、ドメインモデルを作り上げることです。
作ったドメインモデルを正確に表現するコードとして実装し、これを繰り返してドメインモデルとコードの両方を洗練させていく。
ドメインモデルを正確にコードで表現する、言うのは簡単ですが実行するのは難しい。そこでドメイン駆動設計ではそのために役立つ技術的なパターンも紹介されていて、それが戦術的設計に分類される手法です。
アーキテクチャの話のところで出てきた「ドメインを隔離する」というのは、ドメインモデルを正確にコードで表現するために必要ですし、他にも値オブジェクトやエンティティ、サービス、リポジトリなどの手法があります。

重要なのは、これらの技術的なパターン、手法は「ドメインモデルを正確に表現したコードを実装する」ためのものです。表現したいドメインモデルがないのに、技術的なパターンを真似してもドメイン駆動設計の恩恵は限定的です。
ドメイン駆動設計について、多少でもおわかりいただけたでしょうか。

なんとなく理解したけどコードが出てこないとやっぱりよくわからない。 私もそうです。言葉だけで理解するのは難しい。

というわけで、ここからはおまけです。
ドメイン、そしてドメインモデルについて考える練習として、すごく単純な例を用意しました。
動物の体重をTextViewに表示するコードです。 体重がわからない動物のときは空表示にしたいようです。

このコードには2つ問題があります。
1つ目が、-1という数字が特別な意味をもつということをユーザーインタフェースが知ってしまっているということ。
2つ目が、マイナスの体重というものが表現されてしまっていることです。

体重というものについて考えてみてください。体重の概念にマイナスというものはありますか?私の体重マイナスですっていうかたいますかね?いたとしたら多分その人は反物質でできているんだと思います。 ここで大事なのは科学的にありえるかどうかではなく、ドメインについて考えることです。動物の体重という概念を反映したドメインモデルを考えるとマイナスという状況はないですよね。

ではどうするか、体重を反映したドメインモデルを作るんです。そのモデルは浮動小数の値を持っていて、その値はマイナスではない。いや、マイナスではないというより 0 より大きいと考えるほうが適切ですね。

ドメインモデルを考えたので実装してみましょう。

Weight オブジェクトのインスタンスから取得した体重の値は、必ず 0 より大きい。つまり体重に対するドメインモデルを表現できています。
これを利用すると先ほどのコードはこうなります。

注意してほしいのが、未入力を -1 で表すか null で表すかという話ではない、ということです。
ドメインにおける体重というものについて考え、それを反映したドメインモデルを考え、Weight クラスというドメインオブジェクトとして表現する、という話です。
今度は性別を表示するようです。

このコードにも問題がありますね。
文字列が “M” の場合がオスで”F”の場合はメスだそうです。文字列の意味をUIが知っている。なんて賢いUIなんでしょう。

なんでこんな実装になっているのか聞いてみました。
「サーバーのレスポンスが文字列だったので...」
なるほどーーーー。

UIが文字列の意味を知ってるのは変だから直してみてくれる?
「わかりました。できました。ユーティリティクラスを作って、そっちの static メソッドで判定するようにしました!」

そういうことじゃないんだ...


UIがどのユーティリティメソッドを使って判定するか知っていなければならない、結局UIが賢い問題は解決していない。

必要なのはドメイン、つまり性別について考えることです。
我々の頭のなかには性別という概念的なモデルがあるのだから、それを表現するドメインモデルを用意しましょう。
動物の性別を表現するモデルで、オス(MALE)とメス(FEMALE) がある。

このドメインモデルをコードで表現するなら enum でよさそうです。
「サーバーのレスポンスは文字列ですよ。どこで enum に変換するんですか?」

そこで出てくるのがコンテキストの統合で紹介した腐敗防止層です。

ここで、サーバーのレスポンスをアプリのコンテキスト内のドメインモデルに変換します。
長い時間お疲れ様でした。

ドメイン駆動設計について多少なりとも理解いただけたでしょうか。
難しかったかもしれませんが、
まずはドメインについて考えるところから始めてみてください。
そして、大事なのはドメインモデルです。

技術的なパターンだけとりあげたブログなどがよくありますが、表面的なことを真似しただけでうまくいくようなそんな簡単なものではありません。

例であげたようなシンプルなドメインモデルからでいいのです。ドメインを反映したモデルを作るという本質に取り組んでほしいです。
DDDは概念的な話が多くて理解するのが難しいかもしれません。理解しても実行するのは一筋縄ではいかないです。でも怖くはありません。

行き当たりばったりの設計に、とりあえず動けばいいやで作られたアプリを長年メンテナンスし機能追加するほうがよっぽど恐ろしいです。

シンプルなところからぜひドメイン駆動設計の本質を取り入れてみてください。




アプリの Notification がブロックされているかどうかを取得する

$
0
0

結論

NotificationManagerCompat.areNotificationsEnabled()

を使う。

補足

フレームワークに追加されたのは API Level 24 なので、KitKat (API Level 19〜)対応するなら NotificationManagerCompat を使う。
NotificationManager.areNotificationsEnabled()

NotificationManagerCompat では、 KitKat 〜 に対応するため NotificationManagerCompatKitKat に処理がバックポートされている。



Settings - Apps - App Info - Notifications で行ける画面で、矢印の Block all にチェックすると areNotificationsEnabled() の戻り値が false になる。

DrawerLayout を使った画面で StatusBar の色を動的に変える

$
0
0
結論

DrawerLayout の setStatusBarBackground()setStatusBarBackgroundColor()を使う。


補足

DrawerLayout では android:fitsSystemWindows="true"がセットされている場合、自分で StatusBar 部分を描画します。 public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl { ... private Drawable mStatusBarBackground; ... public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); ... if (ViewCompat.getFitsSystemWindows(this)) { IMPL.configureApplyInsets(this); mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); } ... } ... @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDrawStatusBarBackground && mStatusBarBackground != null) { final int inset = IMPL.getTopInset(mLastInsets); if (inset > 0) { mStatusBarBackground.setBounds(0, 0, getWidth(), inset); mStatusBarBackground.draw(c); } } } } そのため Window.setStatusBarColor()で色を変えようとしても意図したようになりません。DrawerLayout のメソッドを使いましょう。



Android UI Test : Dialog を表示する

$
0
0
@RunWith(AndroidJUnit4.class) @LargeTest public class ShowDialogUiTest { // Activity は起動できて副作用がないやつならなんでもよい @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(LoginActivity.class); @Test public void showDialog() throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { // Dialog は Window token のある context が必要なので Context として Activity を使う new AlertDialog.Builder(mActivityRule.getActivity()) .setTitle(R.string.title) .setMessage(R.string.message) .setPositiveButton(android.R.string.yes, null) .setNegativeButton(android.R.string.no, null) .setCancelable(false) .show(); } }); onView(withText(R.string.message)).check(matches(isDisplayed())); } }
ActivityTestRule の対象にする Activity は「androidTest に DummyActivity を用意する」みたいな方法をとりたかったんだけど、アプリとテストで apk が分かれるので無理そうだ ↓ http://stackoverflow.com/questions/36276909/create-dummyactivity-inside-androidtest-folder-for-testing


android コマンドから sdkmanager に移行

$
0
0
android コマンドは廃止になりました。
https://developer.android.com/studio/releases/sdk-tools.htmlの SDK Tools, Revision 25.3.0 (March 2017)

代わりに sdkmanager コマンドを使います。
使い方は
https://developer.android.com/studio/command-line/sdkmanager.html$ sdkmanager "build-tools;25.0.0"のように使います。 複数のパッケージをファイルに羅列して $ sdkmanager --package_file=file のように指定することもできます。 file の中身はこんな感じ platform-tools build-tools;25.0.0 platforms;android-24 platforms;android-25 extras;android;m2repository extras;google;m2repository extras;google;google_play_services extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2 extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2

ライセンスの accept はどうするかというと、
https://developer.android.com/studio/intro/update.html#download-with-gradleにあるように、
ローカルで許可したあとの
[sdk dir]/licenses/android-sdk-license
ファイルをビルドサーバーにコピーしておきます。


What's New in Android Support Library (Google I/O '17)

$
0
0
What's New in Android Support Library (Google I/O '17)
  • v26.0.0-beta1 の話
  • support library の minSdkVersion が 14 になった
  • メソッドやクラスを削減して、メソッドカウントが約1.4k減った
  • 今後より多くメソッドやクラスを削減したいので 約 30 classes / interfaces, 約 400 メソッドが deprecated になった
    • later version で削除される予定
  • Google Maven Repository で配布されるようになった
    • Constraint Layout Library や Architecture Components Library も含まれる
    • 過去の Support Library version (13.0.0 〜) も含まれる
repositories{ maven { // Google Maven Repository url "https://maven.google.com" } } dependencies { compile "com.android.support:appcopmat-v7:26.0.0-beta1" }

XML Font (14+)

  • font を xml で指定できるようになった
  • res/font/font1.ttf, res/font/xml_font.xml
  • font-family で font をグループ化
res/font/myfont.xml <?xml version="1.0" encoding="utf-8"?> <font-family xmlns:app="http://schemas.android.com/apk/res-auto"> <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/myfont_regular" /> <font app:fontStyle="normal" app:fontWeight="800" app:font="@font/myfont_bold" /> </font-family> <TextView ... android:fontFamily="@font/myfont" android:textStyle="bold" /> Typeface typeface = ResourceCompat.getFont(context, R.font.myFont); textView.setTypeface(typeface);

Downloadable Fonts(14+)

  • 今までもフォントファイルをアプリの中に持って使うことができたがアプリサイズが大きくなる要因
  • Font Provider は font を fetch し、cache し、アプリが必要なフォントを返す
  • 複数のアプリから単一のエントリーポイント(FontsContractCompat)を経て Font Provider にアクセスする
    • 複数のアプリでメモリを節約できる
  • Font Provider は Google Play Services を介して 800+ の Google Fonts を利用できる
FontRequest request = new FontRequest( "com.example.fontprovider.authority", "com.example.fontprovider", "Name fo font", R.array.certs); FontsContractCompat.FontRequestCallback callback = new FontsContractCompat.FontRequestCallback() { @Override public void onTypefaceRetrieved(Typeface typeface) {} @Override public void onTypefaceRequestFailed(int reason) {} }; FontsContractCompat.requestFont(context, request, callback, new Handler()); xml/font/downloadedfont.xml <font-family xmlns:app="http://schemas.android.com/apk/res-auto"> app:fontProviderAuthority="com.example.fontprovider.authority" app:fontProviderPackage="com.example.fontprovider" app:fontProviderQuery="dancing-script" app:fontProviderCerts="@array/certs"> </font-family>
  • これをレイアウトに指定したら、フォントを fetch して適用するまでやってくれる
  • 適用できるまでの間はデフォルトのフォントで表示される
  • Android Studio で google fonts からフォントを選択できるようになった(3.0)
    • 自動で downloadedfont.xml が作られる

Emoji Compatibility Library (19+)

tofu 問題を解決するぞ dependencies { compile "com.android.support:support-emoji:${version}" } FontRequest fontRequest = new FontRequest( "com.example.fontprovider", "com.example", "emoji compat Font Query", CERTIFICATES); ); EmojiCompat.Config config = new FontRequstEmojiCompatConfig(this, fontRequest); EmojiCompat.init(config);
  • Google Play Service がないデバイスをターゲットにするには
    • Bundled configuration - 7MB
dependencies { compile "com.android.support:support-emoji-bundled:${version}" } EmojiCompat.Config config = new BundledEmojiCompatConfig(this); EmojiCompat.init(config);
  • android.support.text.emoji.widget.EmojiTextView, EmojiEditText, EmojiButton
    • 自動で Emoji Compat を利用して Emoji を表示する

Autosizing TextView

<TextView ... app:autoSizeTextType="uniform" /> より細かく指定したい場合
xml で定義したサイズの中から一番合うサイズを選択してくれる <TextView ... app:autoSizeTextType="uniform" app:autoSizePresetSizes="@array/autosize_sizes" /> min, max を指定する場合 <TextView ... app:autoSizeTextType="uniform" app:autoSizeMinTextSize="12sp" app:autoSizeMaxTextSize="100sp" app:autoSizeStepGranularity="2sp" />

DynamicAnimation (16+)

final SpringAnimation anim = new SpringAnimation( bugdroidImageView, // object to animate TRANSLATION_Y, // property to animate 0); // equilibrim state anim.getSpring() .setDampingRatio(0.7f /* lower is more bouncy */) .setStiffness(1500f /* higher oscillates faster */) anim.setStartVelocity(velocityTracker.getYVelocity()) .start();

Vector Drawable Compat - FillType (14+)

  • android:fillType
  • Determines "inside" of shape
  • Corresponds to SVG's fill-rule
  • Commonly used by vector drawing tools

Animated Vector Drawable Compat - pathData morphing (14+)

  • Animate <vector> android:pathData attribute
  • Set valueFrom, valueTo using VectorDrawable path data
  • Path formats must match
res/drawable/buffalo.xml <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> res/anim/buffalo_to_hippo.xml <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:propertyName="pathData" android:valueFrom="@string/buffalo" android:valueTo="@string/hippo" android:valueType="pathType" /> res/drawable/animal_morph.xml <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/buffalo"> <target android:name="buffalo_path" android:animation="@anim/buffalo_to_hippo" /> </animated-vector> aapt を使う res/drawable/animal_morph.xml <animated-vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> <aapt:attr name="android:drawable"> <vector android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> </aapt:attr> <target android:name="buffalo_path" android:animation="@anim/buffalo_to_hippo" /> </animated-vector> 全部を1つのファイルにする res/drawable/animal_morph_bundle.xml <animated-vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> <aapt:attr name="android:drawable"> <vector android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> </aapt:attr> <target android:name="buffalo_path"> <aapt:attr name="android:animation"> <objectAnimator android:duration="1000" android:propertyName="pathData" android:valueFrom="@string/buffalo" android:valueTo="@string/hippo" android:valueType="pathType" /> </aapt:attr> </target> </animated-vector>

<pathInterpolator>

  • Parity with platform AVD
  • <objectAnimator> で利用 android:interpolator
  • Used VectorDrawable (SVG-like) path data
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:pathData="M 0.0, 0.0 c 0.08,0.0 0.04,1.0 0.2,0.8 l 0.6,0.1 L 1.0,1.0" />

Wear

New support-wear module

TV

Leanback

PreferenceDataStore

PreferenceDataStore
  • preference storage mechanism のカスタマイズを可能に
class CloudDataStore extends PreferenceDataStore { @Override public void putBoolean(String key, boolean value) { // cache value immediately, launch async task to persist // data to cloud service. } @Override public void getBoolean(String key, boolean defValue) { // Return cached value. return false; } } メソッドは Main スレッドで呼ばれるので注意 // Set up this PreferenceFragment to store // and retrieve data using CloudDataStore. PreferenceManager prefManager = getPreferenceManager(); CloudDataStore cloudStore = new CloudDataStore(); prefManager.setPreferenceDataStore(cloudStore);

FragmentManager

executePendingTransaction(), commitNot(), and similar transaction calls are no longer allowed during FragmentManager state changes.

FrameMetricsAggregator

  • FrameMetricsAggregator
  • Performance monitoring tool used to capture a variety of information about Activity drawing.

ActionBarDrawerToggle

setDrawerSlideAnimationEnabled()メソッドで Navigation drawer のアイコンのアニメーションを無効化できる


What's New in Android Development Tools (Google I/O '17)

$
0
0


Android Studio 3.0

2.4 ではなく 3.0 にした理由
  • incremental change ではないから
  • breaking gradle API change があるから

Develop

最新の IntelliJ stable 27.1 ベース

Kotlin サポート

  • Create Android Project ウィザードに Include Kotlin support チェックボックスが追加
  • 既存のプロジェクトに Kotlin ファイルを直接作成すると自動で project の dependencies が更新
  • [Code] - [Convert Java File to Kotlin File] で既存の Java ファイルを Kotlin に変換
  • show Kotlin Bytecode で Kotlin Bytecode Window を起動し、上部の Decompile ボタンで Java コードをチェック
  • Java で動く lint は Kotlin でも動く

Layout Editor

  • ConstraintLayout 1.0.0 beta1 の機能(barriers など)に対応
新しい sample リソースタイプ tools:text="@tools:sample/lorem" tools:text="@tools:sample/date_day_of_week"表示データ用の json ファイル(hoge_log.json)を用意して tools:src="@sample/hoge_log.json/hoge/icon" tools:text="@sample/hoge_log.json/hoge/description"

Downloadable Font 対応

  • TextView を選択して Properties の fontFamily から More Fonts... を選択
  • font を検索して選択

Adaptive Icon

  • Image Assert ウィザード で Adaptive Icon に対応

Device File Explorer

  • 右下にある
  • デバイス内のファイルを見たり、アップロード、ダウンロードできる

Instant Apps

  • リファクタリングサポート
    • クラスファイルで Modularize... を起動
    • 依存クラス含めてどれを module に移動するか選択
    • 現状 initial version でまだまだ改善中らしい

Apk Analyzer

  • クラスを右クリック - Show bytecode
  • Load proguard mapping ボタンから mapping ファイルを設定
  • クラスを右クリック - Generate Proguard keep rule
  • クラスを右クリック - Find Usages

Apk Debugging

  • [File] - [Profile or Debug APK...]

Profiler

  • Android Profiler ウィンドウ
  • CPU, Memory, Network

Network Profiler

  • Network をクリック、ズームイン、ネットワークリクエストが表示される
  • ネットワークリクエストをクリックすると、詳細と Call Stack が表示される
  • HttpUrlConnection とそれをベースにしているもの(Volleyとか)、OkHttp に対応

CPU Profiler

  • Recording をクリック、アプリを操作、Stop Recording をクリック
  • Thread が表示される

Memory Profiler

  • Garbage Collection
  • Heap dump
  • Recording


Build

Google's maven Repo

  • Gradle plugin
  • Support Libraries
  • Data-Binding Library
  • Constraint Layout
  • Instant Apps Library
  • Architecture Compoennts
  • Android Testing Libraries
  • Espresso
repositories{ maven {url 'https://maven.google.com'} or google() // 4.0-milestone-2 or later }

Build Performance

  • Incremental Tasks
    • Resource Processing(planned for 3.0)
    • Shrinking (experimental, 2.3)
    • Java Compilation (Gradle 3.5)
    • Dexing (3.0)
    • Annotation Processor x
    • APK Packaging (2.2)

Build Cache

  • Local
    • Switch between branches without recompiling -- build-cache org.gradle.caching=true
  • Distributed
    • Share with team members And Build Server
    • Hazelcast implementation Gradle Enterprise API for custom backend

Multi-modules Advantages

  • Code Reuse
  • Improve Caching
  • API/Dependency Control
  • Compilation Avoidance
複数のモジュールでの Parallel Build に取り組んでいる

Support for Java 8 Language Features

android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } defaultConfig.jackOptions.enabled=true は廃止

Dependency Management

  • いままでは依存ライブラリ(module) の release build が利用されていた
  • 手動で buildType を合わせる方法もあったが割と設定が大変
  • 3.0 では新しい gradle の API によって同じ名前の buildType や flavor があれば自動で合わせてくれるようになった
  • ライブラリに flavor があってアプリ側に対応する flavor がない場合のために、新しい flavorSelection が追加
  • flavor が一つでも flavorDimension が必須に
アプリ側 android { flavorSelection 'color', 'blue' } ライブラリ側 android { flavorDimension 'color' productFlavors { blue {} orange {} } }

Instant Apps

  • feature module (com.android.feature) に分割
  • feature module を組み合わせて Instant-app (com.android.instantapp) を作る


Test

Android Emulator

  • Google Play Store が搭載された Google Play Edition が増えた
  • Open GL ES 3.0
  • Proxy Support

  • Android Open Source Project
    • x : Google APIs
    • x : Google Play
    • o : Elevated Privileges
  • Google Play Edition
    • o : Google APIs
    • o : Google Play
    • x : Elevated Privileges (root access など無し)

App Bug Reporting

  • エミュレータからスクリーンショットなど情報つきでバグレポートをQAチームなどに簡単に送れる

Android Wear Emulator

  • Emulator Rotary Input support

Layout Inspector

  • いろいろ改善した

Optimize

  • Android Profiler (CPU, Memory, Network)
  • APK Analyzer
  • WebP support



What's New in Android Design Tools (Google I/O '17)

$
0
0


# What's new ではない復習的な内容は一部省略しています。

ConstraintLayout

  • Unified and expressive way to create Layouts
  • Flat Layouts
  • Deep INtegration with Android Studio & the Layout Editor
  • Unbundled Library
  • Compatible with 99% of Android Devices
1.0
  • Google I/O 2016 から17回リリース
  • 2017年2月に 1.0 リリース
  • パフォーマンス改善
  • Relative positioning
  • Center positioning & bias
  • Guidelines
  • Chains
  • Ratio
  • ConstraintSet
Android Studio でプロジェクトを作った時のデフォルトが ConstraintLayout に

コミュニティベースのサイトがオープン https://constraintlayout.com/

1.1.0 beta1 maven { url "https://maven.google.com" } dependencies { compile "com.android.support.constraint:constraint-layout:1.1.0-beta1" }
  • Barriers
    • widget の集まりに対して、最小 or 最大の端を取るもの
  • Group
    • widget の集まりを定義できる
    • group に対して setVisibility() すると、それに含まれるすべての widget に setVisibility() される
  • Placeholder

Placeholder

virtual view を作成し、ConstraintLayout の別の view を contents としてセットできる TransitionManager.beginDelayedTransition(container); placeholder.setContentId(view.getId()); 縦横のレイアウトを Placeholder を使った template として用意し、メインのレイアウトを1つにできる layout/header_template.xml <merge ... android:layout_width="match_parent" android:layout_height="match_parent" tools:parentTag="android.support.constraint.ConstraintLayout"> <android.support.constraint.Placeholder android:id="@+id/template_main_image" app:content="@+id/top_image" app:layout_constraintDimensionRatio="16:9" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> <android.support.constraint.Placeholder android:id="@+id/template_action" android:layout_width="48dp" android:layout_height="48dp" app:content="@+id/action_button" app:layout_constraintBottom_toBottomOf="@id/template_main_image" app:layout_constraintHorizontal_bias="0.80" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/template_main_image" /> </merge> layout-land/header_template.xml <merge ... android:layout_width="match_parent" android:layout_height="match_parent" tools:parentTag="android.support.constraint.ConstraintLayout"> <android.support.constraint.Placeholder android:id="@+id/template_main_image" app:content="@+id/top_image" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" app:layout_constraintTop_toTopOf="parent" /> <android.support.constraint.Placeholder android:id="@+id/template_action" android:layout_width="48dp" android:layout_height="48dp" app:content="@+id/action_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@id/template_main_image" app:layout_constraintRight_toRightOf="@id/template_main_image" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.80" /> </merge> layout/activity_main.xml <android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.sample.myapplication.MainActivity"> <ImageView android:id="@+id/top_image" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageButton android:id="@+id/action_button" android:layout_width="48dp" android:layout_height="48dp" /> <include layout="@layout/header_template" /> </android.support.constraint.ConstraintLayout> template に割り当てる部分は ViewGroup や include でも問題ない <android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.sample.myapplication.MainActivity"> <ImageView android:id="@+id/top_image" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <include android:id="@+id/action_button" layout="@layout/action_button_content" /> <include layout="@layout/header_template" /> </android.support.constraint.ConstraintLayout>

ConstraintSet

  • View 自体と、それをどのようにレイアウトするかを分離
  • すべての制約を一つのオブジェクトにカプセル化
  • 既存の ConstraintLayout に ConstraintSet を適用できる
  • 複数の ConstraintSet 間を切り替え
    • layout を切り替えるわけではないので、view は reload されない
ConstraintSet mConstraintSet1 = new ConstraintSet(); ConstraintSet mConstraintSet2 = new ConstraintSet(); // get constraints from layout mConstraintSet2.clone(context, R.layout.state2); setContentView(R.layout.state1); mConstraintLayout = (ConstraintLayout) findViewByid(R.id.activity_main); // get constraints from ConstraintLayout mConstraintSet1.clone(mConstraintLayout); // switch with animation TransitionManager.beginDelayedTransition(mConstraintLayout); // switch to state2 mConstraintSet2.apply(mConstraintLayout);
その他の利用例) 縦横それぞれのレイアウトとその ConstraintSet を用意し、画面回転を自分でハンドリングして、そのときに ConstraintSet を切り替えることで自分でレイアウトを切り替えることが可能


ConstraintLayout & Motion
  • Flat Hierarchy == No clipping issues
  • Scene Graph
  • ConstraintSet == Keyframe


Android Studio 3.0

Tools がいろいろある
  • alignment tools
  • arrengement tools
  • guideline tools
  • right click menu
Inference
  • 接続の確率モデルに基づく
  • 制約されていない view を制約する(すでに制約されているものは何もしない)
  • view は動かさない(alignment tools ではない)
Advanced Inspector
  • properties がどの layout, dimen, strings 由来なのか表示
Tools attributes https://developer.android.com/studio/write/tool-attributes.html
  • tools:
    • デザイン時の属性を上書き
  • tools:showIn
    • このレイアウトを他のやつに埋め込んで表示する
  • tools:layout
    • fragmentで利用するレイアウト
  • tools:listitem
    • list item のレイアウト
  • tools:parentTag
    • merge tag の parent のレイアウトを指定

Sample Data

* Sample Data は Android Studio 3.0 で利用可能になる予定ですが、残念ながら現状(Canary 2)ではまだ使えません。

  • Default Adapter で使える
  • content を指定する
  • tools:listitem で list item のレイアウトを指定する
  • list item のレイアウトで sample data を使う tools:text="@tools:sample/lorem" tools:text="@tools:sample/full_names" tools:text="@tools:sample/us_phones" tools:text="@tools:sample/date_mmddyyyy"
  • sampledata フォルダーを作る(app > sampledata)
  • フォルダーに colors ファイルを追加 #d50000 #2962ff #00e5ff #aeea00 #ff6d00 tools:background="@sample/colors"
  • フォルダーに json(contacts.json とか)ファイルを追加 { "contacts": [ { "name":"...", ... } ] } tools:text="@sample/contacts.json/contacts/name"
  • Baked-in data types
    • date, names, phone numbers...
  • JSON files
  • Resources in sample data folder
    • Text
    • Color
    • Image (collections if in folder)



Viewing all 415 articles
Browse latest View live