分類
Android

App Bar 簡介和加入 App Bar 到 Activity

以下內容為參考官網以及實際實作的心得。

App Bar 是什麼?

App Bar (也稱為Action Bar) 是Activity重要的設計元素之一,因為它提供了使用者熟悉的視覺結構和交互元素。使用App Bar可讓App的行為和外觀保持一致,讓使用者能夠快速了解如何操作並獲得良好的體驗。
App Bar的主要功能為
a.為App提供名稱並顯示目前在App中的位置。
b.提供重要操作,例如搜尋。。
c.支持導航和視圖切換。
從Android 3.0(API Level 11)開始,使用預設主題的所有activity都將使用Action Bar作為App Bar。但Action Bar的功能是在各個Android版本中逐漸加入,因此Action Bar的行為可能會有所不同,實際上取決於設備使用的Android版本。
相比之下,在android support library的Tool Bar已加入了最新功能,並且可以在任何可以使用支持函式庫的設備上使用。
因此,應該使用android support library的Tool Bar來實現App Bar,這種方式有助於App在大多數的設備上具有一致的行為。

加入 Tool Bar 到 Activity

1.加入 androidx appcompat library,因為Tool Bar位於該函式庫內
Note:從Android 9開始,Google推薦使用androidx library,因此這裡使用的是androidx,而不是v7 appcompat support library。
詳細內容參考https://developer.android.com/topic/libraries/support-library/setup

Project 的build.gradle加入

buildscript {
    repositories {
        google()
        jcenter()
...
    }
}
allprojects {
    repositories {
        google()
        jcenter()
   ...
    }
}

Module的build.gradle加入

dependencies {
   ...
    implementation 'androidx.appcompat:appcompat:1.0.2'
...
}

 
2.讓Activity繼承 AppCompatActivity

import androidx.appcompat.app.AppCompatActivity;
public class BasicToolBarActivity extends AppCompatActivity {
...
}

 
3. 設定AndroidManifest.xml的 <application> 元素使用appcompat的 NoActionBar 主題
Note:改用NoActionBar的目的就是不使用預設的Action Bar

<application
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    />
...

 
4.加入 Tool Bar 到 Activity 的佈局檔中。
有兩種做法,第一種是直接在Activity的佈局檔中加入(寫死的做法),這種做法的缺點是當要修改Tool Bar時,所有的Activity寫死的部分都要修改。如下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".basictoolbar.BasicToolBarActivity">
  <androidx.appcompat.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
  </androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>

第二種做法是另外建立一個Tool Bar的佈局檔,然後在Activity的佈局檔中使用<include>加入Tool Bar的佈局檔。比較建議這種做法。
如下新增app/src/res/main/layout/toolbar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  android:background="?attr/colorPrimary"
  android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
</androidx.appcompat.widget.Toolbar>

接著在Activity的佈局檔中使用<include>引入Tool Bar的佈局檔
/app/src/main/res/layout/activity_basic_tool_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".basicaction.BasicActionActivity">
  <include
    android:id="@+id/app_toolbar"
    layout="@layout/toolbar_main"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

 
5.在Activity的 onCreate() 中呼叫 setSupportActionBar() 方法並傳入在佈局檔中的Tool Bar物件

package com.codefoxx.toolbarexample.basictoolbar;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import com.codefoxx.toolbarexample.R;
public class BasicToolBarActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_tool_bar);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
  }
}

 
完成,現在就是使用 Tool Bar 作為 App Bar。在預設的情況下會有App的名稱以及overflow menu。
另外使用Tool Bar作為App Bar時就可以呼叫androidx appcompat library的Action Bar類別所提供的方法。
具體的做法是呼叫 getSupportActionBar 方法,該方法返回Action Bar參考,取得Action Bar參考之後便可操縱Tool Bar。如下在程式碼中重新設定App Bar的 title

public class BasicToolBarActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_basic_tool_bar);
    Toolbar toolBar = findViewById(R.id.app_toolbar);
    setSupportActionBar(toolBar);
    ActionBar supportActionBar = getSupportActionBar();
    supportActionBar.setTitle("set customize title bar");
  }
}


以上便是加入基本的Tool Bar到Activity的方式。
以下是關於App Bar其他操作。
http://34.80.81.192/?p=6165
http://34.80.81.192/?p=6176
http://34.80.81.192/?p=6192
 

分類
Android

ConstraintLayout

ConstrainLayout 是什麼?

ConstraintLayout可以用來製作大型複雜的布局且沒有巢狀視區群組,類似於RelativeLayout但又比RelativeLayout更有彈性。
ConstraintLayout目前(20190816)已被移至androidx 套件中,套件位置可參考。https://dl.google.com/dl/android/maven2/index.html(穩定版本為1.1.3,beta版本為2.0.0-beta2)
ConstraintLayout可用於API level 9以上可以適用於大多數的裝置。ConstraintLayout操作方式可以透過佈局編輯器的可視化工具來編輯也可以直接修改佈局檔。
 

ConstraintLayout 概述

在ConstraintLayout中的視圖元件(view)最少需要定義一個水平方向和一個垂直方向的約束,每個約束的目標對象可以是另一個視圖,父元件或引導線等等。
如下就是TextView具有水平方向以及垂直方向的約束,水平方向約束為左邊貼齊父元件左邊,垂直方向約束為上邊貼齊父元件上邊。

<TextView
  android:id="@+id/constraint_layout_tv_left_top_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="lt to lt and tp to tp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

官方介紹參考
https://developer.android.com/reference/android/support/constraint/ConstraintLayout#Chains
https://developer.android.com/training/constraint-layout
官方範例
https://github.com/googlesamples/android-ConstraintLayoutExamples
 

ConstraintLayout 相依性

在project的build.gradle加入google repo

buildscript {
    repositories {
        google()
    }
}
allprojects {
    repositories {
        google()
    }
}

在Module的build.gradle加入

dependencies {
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
 }

 

CostraintLayout 的約束類型

Relative Position(相對定位)
Margin(邊距)
Circular positioning(圓狀定位)
Chains(鏈)
Virtual Helper objects(虛擬幫助物件)
另外注意不能在約束中有循環依賴關係。
可以參閱ConstraintLayout.LayoutParams以獲取佈局屬性
 

Relative Position 相對定位

相對定位是根據目標元件設定位置的定位方法,目標元件可以是某個元件或父元件或是虛擬幫助物件(引導線或分界線)。
Relative Position和Relative Layout的用法相當類似,RelativeLayout也是透過android:layout_above等等來指定元件相對於另一個元件的位置。
相對定位的種類可以分為水平和垂直,水平定位有right, left, start, end,垂直定位有top, bottom, baseline。
圖示左邊為垂直定位,右邊為水平定位

可以使用的屬性如下

layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf

可以把屬性解釋為
layout_constraint目前元件邊_to目標元件邊Of=”目標元件”
範例如下

<TextView
  android:id="@+id/constraint_layout_tv_left_top_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="lt to lt and tp to tp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

第6行代表目前元件的左邊貼齊目標元件(父元件)的左邊。
第7行代表目前元件的上邊貼齊目標元件(父元件)的上邊。
圖示如下

目標元件也可以改為某個元件,使用時只要指定目標元件的id即可。
現在新增另一個元件(top to bottom and left to left),該元件的上邊會貼齊目標元件(lt to lt and tp to tp)的下邊,左邊貼齊左邊如下

  <TextView
    android:id="@+id/constraint_layout_tv_left_top_parent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="lt to lt and tp to tp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  <TextView
    android:id="@+id/constraint_layout_tv_top_bottom"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="top to bottom and left to left"
    app:layout_constraintTop_toBottomOf="@+id/constraint_layout_tv_left_top_parent"
    app:layout_constraintLeft_toLeftOf="@+id/constraint_layout_tv_left_top_parent"/>

圖示如下

需要注意的是水平置中為

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"

垂直置中為

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

水平及垂直置中

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

 

Margins 邊距

邊距是用來增加指定邊的距離,需要注意的是想增加的邊距必須是已存在相對的約束才有效果。如下
假設目標元件(center parent)已被設定為垂直和水平置中於父元件

<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".margins.MarginsActivity">
  <TextView
    android:id="@+id/margins_activity_tv_center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="center parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

現在有一元件(margin top 50)下邊貼齊center parent的下邊,左邊貼齊左邊。如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintBottom_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  />

margin top 50元件想增加上邊距50dp,把增加上邊距50dp加入如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintBottom_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  />

這種做法沒有效果,因為目前元件(margin top 50)並沒有對目標元件有Top的約束,只有Bottom和Left的約束。
因此想讓android:layout_marginTop=“50dp”有效果,必須在目標元件內加入constraintTop如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintTop_toTopOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  />


至於是constraintTop_toTopOf還是constraintTop_toBottomOf都可以,只要目標元件具有constraintTop的約束即可。
以下是可以使用的margin

android:layout_marginStart
android:layout_marginEnd
android:layout_marginLeft
android:layout_marginTop
android:layout_marginRight
android:layout_marginBottom

注意margin只能接受大於等於0的數值。
另外當目標元件的可視性為gone時,可以透過layout_goneMarginXXX設定對應的邊距。接續上面的範例,若希望margin top 50在center parent的可見性為gone時,增加上邊距改為100,如下

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="margin top 50"
  android:textColor="@color/colorAccent"
  app:layout_constraintTop_toBottomOf="@id/margins_activity_tv_center"
  app:layout_constraintLeft_toLeftOf="@id/margins_activity_tv_center"
  android:layout_marginTop="50dp"
  app:layout_goneMarginTop="100dp"
  />

 

Centering positioning and bias 置中定位和偏移

置中定位即為水平置中以及垂直置中。
水平置中為

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"

垂直置中為

app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

水平及垂直置中

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

 

bias 偏移

當單純使用水平置中或是垂直置中會讓目標元件置中也就是讓目標元件位於中心位置,但若想讓目標元件不這麼置中,而想偏移一些位置時就可以使用bias。
水平置中偏移為
layout_constraintHorizontal_bias
當水平置中偏移為0.5時就是預設的水平偏移位置。
可以把水平置中偏移當作從左邊移到右邊的位置,數值越靠近0就越靠近左邊,越靠近1就越靠近右邊
垂直置中偏移為
layout_constraintVertical_bias
當垂直置中偏移為0.5時就是預設的垂直偏移位置
可以把垂直置中偏移當作從上邊移到下邊的位置,數值越靠近0就越靠近上邊,越靠近1就越靠近下邊
範例如下
假設目前元件已經被設定為垂直置中了

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center parent"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintTop_toTopOf="parent"
  />

現在想讓目標元件靠近上邊則可如下設定

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center to ver"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintTop_toTopOf="parent"
  app:layout_constraintVertical_bias="0.1"
  />


 
水平偏移也是相同的做法。但需要注意以下的限制。
偏移必須有相對的約束才有效。
垂直偏移必須在constraintBottom 以及 constraintTop都存在的情況才有效。
水平偏移必須在constraintLeft以及 constraintRight都存在的情況才有效。

Circular positioning (1.1) 圓狀定位

可以透過角度和半徑來約束目標元件和目前元件的位置。使用方法如下
layout_constraintCircle : references another widget id
layout_constraintCircleRadius : the distance to the other widget center
layout_constraintCircleAngle : which angle the widget should be at (in degrees, from 0 to 360)
基本上就是決定目標元件,半徑,角度。
範例如下

<TextView
  android:id="@+id/center_parent"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="center parent"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintRight_toRightOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>
<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="current"
  app:layout_constraintCircle="@id/center_parent"
  app:layout_constraintCircleAngle="360"
  app:layout_constraintCircleRadius="100dp"/>

current元件會把center parent當作中心點,移動到角度為360度,半徑為100dp的位置

 

Dimensions constraints 尺寸約束

可以為ConstraintLayout定義最大和最小的寬度和高度。如下
android:minWidth   -> set the minimum width for the layout
android:minHeight  -> set the minimum height for the layout
android:maxWidth  -> set the maximum width for the layout
android:maxHeight  -> set the maximum height for the layout
 
Widgets dimension constraints 元件尺寸約束
元件的尺寸(width and height)可以透過3種方式來設定
a. 使用1個指定的尺寸
b. 使用wrap_content以符合內容大小
c. 使用0dp等同於MATCH_CONSTRAINT,會受margin影響
c. 用法比較特別需要另外注意。
當width = 0dp,constraintLeft_toLeftOf = “parent” 且 constraintRight_toRightOf =”parent”,代表元件寬度會填滿父元件的寬度。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".dimensionsconstraint.DimensionConstraintActivity">
  <Button
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="top"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


0dp會受margin影響,增加左右兩邊margin 50dp如下

<Button
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:text="top"
  android:layout_marginLeft="50dp"
  android:layout_marginRight="50dp"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintRight_toRightOf="parent"
  />


 
若約束參考到另一個元件,其width & height也會受影響,如下

<Button
  android:id="@+id/dimension_constrraint_btn_left_top"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="left top"/>
<Button
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:text="top center"
  app:layout_constraintLeft_toRightOf="@+id/dimension_constrraint_btn_left_top"
  app:layout_constraintRight_toLeftOf="@+id/dimension_constrraint_btn_right_top"/>
<Button
  android:id="@+id/dimension_constrraint_btn_right_top"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="right top"
  app:layout_constraintTop_toTopOf="parent"
  app:layout_constraintRight_toRightOf="parent"/>

top center雖然width是0dp,但其左右約束都參考到其他元件,因此不會填滿父元件。

建議在ConstraintLayout的元件不要使用MATCH_PARENT,相同的效果可以透過MATCH_CONSTRAINT加上left, right, top, bottom來達到
 
WRAP_CONTENT:強制約束(1.1
如果尺寸設置為WRAP_CONTENT,則在1.1之前的版本中約束不會限制尺寸。
在某些情況下希望使用WRAP_CONTENT且強制執行約束以限制結果。可以透過以下的屬性達成:
app:layout_constrainedWidth=”true|false”
app:layout_constrainedHeight=”true|false”
 
MATCH_CONSTRAINT尺寸(1.1
當尺寸設置為MATCH_CONSTRAINT時,預設會占用所有空間。 還有幾個額外的屬性可以使用:
layout_constraintWidth_min和layout_constraintHeight_min:將設置尺寸的最小大小
layout_constraintWidth_max和layout_constraintHeight_max:將設置尺寸的最大大小layout_constraintWidth_percent和layout_constraintHeight_percent:將設置尺寸的大小設置為父元件的百分比
Min and Max
Min和max的單位可以設定為xxdp,或是wrap代表wrap_content。

Percent dimension 百分比尺寸

Percent dimension類似於LinearLayout中的設定weight的設定,使用百分比尺寸需要設定以下的內容。
1.尺寸(width or height)必須設定為0dp(match_constraint)
2.app:layout_constraintWidth_default=”percent” 和app:layout_constraintHeight_default=”percent”的預設值應該設定為percent
3.layout_constraintWidth_percent or layout_constraintHeight_percent 為0到1之間的數值
範例如下,設定3個TextView寬度各佔其父元件的0.33333

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <TextView
    android:id="@+id/top_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="top"
    android:textSize="100sp"
    android:background="@color/colorPrimary"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  <TextView
    android:id="@+id/down_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="middle"
    android:textSize="100sp"
    android:background="@color/colorAccent"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/top_button"
    />
  <TextView
    android:id="@+id/middle_button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="bottom"
    android:textSize="100sp"
    android:background="@color/colorPrimaryDark"
    app:layout_constraintHeight_percent="0.33333"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/down_button"
    />
</androidx.constraintlayout.widget.ConstraintLayout>

圖示如下

 

Ratio(寬高比)

可以設定元件的寬高比,首先設定其中一個方向(寬或高)為0dp,再透過layout_constraintDimensionRatio 設定比例,如下

  <Button
    android:layout_width="0dp"
    android:layout_height="100dp"
    android:text="bottom button"
    app:layout_constraintDimensionRatio="2:1"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    />

因為ratio為2:1,而width為0dp,代表width會隨著height變化,而height為100dp,因此width為200dp。
若將width或height都設定為0dp,系統會嘗試滿足最大的比例如下

  <Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:text="top button"
    app:layout_constraintDimensionRatio="2:1"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

設定ratio為2:1,width和height都為0dp。此時系統會讓width為填滿父元件,而height為width的一半。
圖示如下

chains 

鏈提供了對單一方向的元件群組。另一個方向可以獨立變化。
若是一個元件群組被雙向(反方向)連結在一起,就可視為1個鏈,如下圖。

 
Chain heads 鏈首
鏈首為鏈的第一個元素,水平方向的鏈首為最左邊的元件,垂直方向的鏈首為最上方的元件。

Margins in chains 鏈中的邊距

如果鏈中指定了邊距,會套用在鏈中的每個元件之間。使用邊距時,其效果是相加的。
若一個水平鏈有一個元素設定右邊距為10dp,而下一個元素的左邊距為5dp則這兩個元素的邊距為15dp
 
Chains Style 鏈的類型

可以在鏈首元素設定layout_constraintHorizontal_chainStyle or layout_constraintVertical_chainStyle以指定鏈的類型,預設是CHAIN_SPREAD。
以下是鏈的類型
CHAIN_SPREAD
鏈中的元素將會展開(預設),範例如下,2個按鈕的水平鏈

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 
Weighted chain
在CHAIN_SPREAD中,如果有元素設定為MATCH_CONSTRAINT(0dp)將會使用剩下的空間。
範例如下,2個按鈕的水平鏈,first按鈕的width為0dp,因此會占用剩下來的空間。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_SPREAD_INSIDE
在鏈的2端不會有空隙。
範例如下,2個按鈕,設定為chain style設定為spread_inside

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_PACKED
鏈內的元件將會被連結在一起,不會有空隙。
範例如下,2個按鈕,chain style設定為 packed

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="packed"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


CHAIN_PACKED_WITH_BIAS
鏈內的元件會被連結在一起且會透過bias屬性設定比例。
範例如下,2個按鈕,chain style設定為package,且鏈首設定 Horizontal_bias 為 0.2

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_bias="0.2"
    app:layout_constraintHorizontal_chainStyle="packed"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 

 
Weighted chains 權重鏈
鏈中的元素若使用MATCH_CONSTRAINT(0dp),預設會占用所有剩餘的可用空間。
可以加上layout_constraintHorizontal_weight 或 layout_constraintVertical_weight屬性來指定占用空間的比例。
範例如下,2個按鈕,個別設定layout_constraintHorizontal_weight為0.7,0.3

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="0.7"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    app:layout_constraintHorizontal_weight="0.3"
    />
</androidx.constraintlayout.widget.ConstraintLayout>


 
另外一個重要使用方式為占用的總空間可以指定為其他元件,不一定只能是父元件,可以透過改變鏈首起始邊的參考元件以及鏈尾結束邊的參考元件。
如下新增2個Guideline,位置各在水平方向的0.2(guideline_0.2)以及0.8(guideline_0.8)
修改first按鈕的起始邊參考到guideline_0.2,以及second按鈕的結束邊參考到guideline_0.8

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".Chains.ChainsActivity">
  <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline_0.2"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.2"/>
  <Button
    android:id="@+id/activity_chains_first"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="first"
    app:layout_constraintLeft_toLeftOf="@+id/guideline_0.2"
    app:layout_constraintRight_toLeftOf="@+id/activity_chains_second"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="0.3"
    />
  <Button
    android:id="@+id/activity_chains_second"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="second"
    app:layout_constraintLeft_toRightOf="@+id/activity_chains_first"
    app:layout_constraintRight_toRightOf="@+id/guideline_0.8"
    app:layout_constraintTop_toTopOf="@id/activity_chains_first"
    app:layout_constraintHorizontal_weight="0.7"
    />
  <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline_0.8"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.8"/>
</androidx.constraintlayout.widget.ConstraintLayout>


 

Virtual Helper objects 虛擬幫助物件

在ConstraintLayout可以使用Guideline(引導線)和Barrier(分界線)來幫助元件定位。
Guideline(引導線)
Guideline 主要是用於幫助其他元件定位,可以先在佈局檔中建立Guideline,讓其他元件根據Guideline設定位置,Guideline不會顯示在介面上,Guideline可以是水平或垂直方向。以下是一個最簡單的Guideline

<androidx.constraintlayout.widget.Guideline
  android:id="@+id/middle_guide_line"
  android:layout_width="0dp"
  android:layout_height="0dp"
  android:orientation="horizontal"
  app:layout_constraintGuide_percent="0.5"/>

有幾個屬性需要注意, Guideline會有id以讓其他元件定位,width和height通常設為0dp,orientation必須設定為vertical或horizontal以指定水平或垂直方向。
重點為Guide_percent,該值代表Guideline位於畫面上的哪個比例位置,數值為從0到1的小數點。
若是水平方向的Guideline,0為最上方,1為最下方,垂直方向的Guideline 0為最左方,1為最右方,如上例是一個percent為0.5且水平方向的Guideline代表位於畫面中心的橫線。

新增按鈕並讓其上邊貼齊Guideline的上邊

<Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="top middle guide line"
  app:layout_constraintTop_toTopOf="@id/middle_guide_line"
  />


比較複雜的畫面通常需要先對畫面進行規劃,就適合先建立Guideline再讓其他的元件根據Guideline進行定位。
Barrier(分界線)
Barrier和Guideline類似不會顯示,也是用來幫助其他元件定位用,主要的差別在於Barrier通常是根據多個目標元件定位,定位之後會根據目標元件的變化動態改變自己的位置。
範例如下,在畫面左側先加入Account和Password 2個EditText,再增加Barrier,Barrier會定位在Account和Password的右邊,最後加入TextView會定位在Barrier的右邊。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".barrier.BarrierActivity">
  <EditText
    android:id="@+id/account"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="Account : "
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toTopOf="parent"
    />
  <EditText
    android:id="@+id/password"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="Password : "
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toBottomOf="@+id/account"
    app:layout_constraintLeft_toLeftOf="parent"
    />
  <androidx.constraintlayout.widget.Barrier
    android:id="@+id/left_barrier"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="horizontal"
    app:barrierDirection="right"
    app:constraint_referenced_ids="account, password"
    />
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="position to barrier"
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintTop_toTopOf="@+id/account"
    app:layout_constraintLeft_toRightOf="@+id/left_barrier"/>
</androidx.constraintlayout.widget.ConstraintLayout>

圖示如下

因為Barrier根據Account和Password定位,只要這2個元件有變化,則Barrier就會跟著變化。
在Account輸入一連串內容以改變Barrier位置,Barrier位置改變之後position to barrier也會改變。

 
 

分類
Android

綁定型服務

綁定型服務是什麼?

綁定型服務是服務的一種,可以綁定應用程式元件並透過已定義的介面和元件互動。元件可以透過該介面呼叫服務的方法。
 

實作綁定型服務

透過繼承Service類別來建立綁定型服務,需要實作onBind方法,就是最簡單的綁定型服務。

public class BindService extends Service {
  public BindService() {
  }
  @Override
  public IBinder onBind(Intent intent) {
    // TODO: Return the communication channel to the service.
    throw new UnsupportedOperationException("Not yet implemented");
  }
}

重點為onBind方法,該方法會在元件綁定服務時執行,參數intent會從元件傳遞進來以攜帶需要從元件傳入的資料。
onBinder方法會回傳IBinder,當IBinder回傳到元件之後,元件便可透過IBinder和服務互動。
 

實作IBinder

IBinder是一個介面,其用途為讓元件取得服務的參考以進行和服務之間的互動。
因為Android已提供Binder類別,Binder類別已經實作IBinder介面中重要的方法。所以我們不必自己再實作IBinder介面,只需要建立Binder的子類別,並在該子類別中傳回服務的參考即可。

public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

ServiceBinder類別的getBindService方法會回傳服務本身,元件就是透過這個方法取得服務的參考並與其互動。

實作ServiceConnection

當元件和服務進行綁定時是透過呼叫bindService方法來執行,如下
bindService(IntentToService, serviceConnect, Context.BIND_AUTO_CREATE);
該方法第2個參數為ServiceConnect物件,元件透過ServiceConnect物件取得目前和服務綁定的狀況(已連結或已中斷),我們需要建立ServiceConnect物件並實作2個方法,分別為

@Override
public void onServiceConnected(ComponentName name, IBinder service) {}

onServiceConnected方法會在元件綁定服務時被呼叫,該方法的IBinder參數即為從服務的onBind方法回傳,我們就可以透過該參數來取得服務。

@Override
public void onServiceDisconnected(ComponentName name) {}

onServiceDisconnected方法會在元件和服務中斷時被呼叫。
建立BindServiceActivity如下

public class BindServiceActivity extends AppCompatActivity {
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
  }
}

接著在ServiceConnection的onServiceConnected方法中實作透過傳入的binder取得服務的參考。

private BindService mBindService;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
  }
};

另外在元件中還需要一個布林變數來記錄目前是否已綁定服務,這個變數主要是方便元件對服務進行綁定或解綁定。

private boolean mIsServiceBound;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
    mIsServiceBound = true;
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
    mIsServiceBound = false;
  }
};

 

綁定服務或解綁定服務

以Activity來說綁定服務的實作位置通常會在onCreate或onStart,而解綁定的位置會對應綁定的位置。
若Activity移到後台時也要持續從服務收到更新,那就在onCreate綁定然後在onDestroy解綁定。
若Activity只有在前台顯示時需要收到服務的更新,在後台時不需要。就在onStart綁定然後在onStop解綁定。
以下在onCreate綁定服務,在onDestroy解綁定服務。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_bind_service);
  Intent bindService = new Intent(this, BindService.class);
  if (!mIsServiceBound) {
    bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
  }
}
@Override
protected void onDestroy() {
  super.onDestroy();
  if (mIsServiceBound) {
    unbindService(mServiceConnection);
    mIsServiceBound = false;
  }
}

最後在Activity加入按鈕,按鈕按下時就會從服務取得亂數

@Override
public void onClick(View v) {
  int uiID = v.getId();
  switch (uiID) {
    case R.id.bind_service_get_service_random_number:
      if (mIsServiceBound) {
        Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
      }
      break;
  }
}

 
以下為完整程式碼

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumber;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}
public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

 

綁定型服務的生命週期

在BindService複寫onCreate, onBind, onStartCommand, onUnbind, onDestroy加入log來觀察生命週期。

public class BindService extends Service {
…
  public BindService() {
    Log.d(TAG, "BindService: ");
  }
  @Override
  public void onCreate() {
    Log.d(TAG, "onCreate: ");
    super.onCreate();
  }
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  public IBinder onBind(Intent intent) {
    Log.d(TAG, "onBind: ");
    return mBinder;
  }
  @Override
  public boolean onUnbind(Intent intent) {
    Log.d(TAG, "onUnbind: ");
    return super.onUnbind(intent);
  }
  @Override
  public void onDestroy() {
    Log.d(TAG, "onDestroy: ");
    super.onDestroy();
  }
…
}

在BindServiceActivity複寫onCreate, ServiceConnection的onServiceConnected, onServiceDisconnected加入log觀察生命週期

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      Log.d(TAG, "Activity call bindService: ");
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      Log.d(TAG, "Activity call unbindService: ");
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

啟動Activity後的log為

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

需要注意的是onServiceConnected方法是在服務呼叫onBind方法之後呼叫
點擊back key之後的log為

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

需要注意的是當元件透過呼叫unbindService去解綁定服務時, ServiceConnection的onServiceDisconnected方法不會被呼叫。
 

多個元件綁定和解綁定服務

綁定型服務可以和多個元件一起綁定。和多個元件綁定之後只有當所有的元件都解綁定該服務才會銷毀。
為了測試多個元件綁定服務,新增AnotherBindServiceActivity,如下

public class AnotherBindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = AnotherBindServiceActivity.class.getSimpleName();
  private Button mBindServiceBtn;
  private Button mUnbindServiceBtn;
  private Button mGetRandomNumberBtn;
  private Button mLaunchBindServiceActivityBtn;
  private boolean mIsServiceBound;
  private BindService mBindService;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_another_bind_service);
    mLaunchBindServiceActivityBtn = findViewById(R.id.another_bind_service_start_bind_service_activity);
    mLaunchBindServiceActivityBtn.setOnClickListener(this);
    mBindServiceBtn = findViewById(R.id.another_bind_service_bind_service);
    mBindServiceBtn.setOnClickListener(this);
    mUnbindServiceBtn = findViewById(R.id.another_bind_service_unbind_service);
    mUnbindServiceBtn.setOnClickListener(this);
    mGetRandomNumberBtn = findViewById(R.id.another_bind_service_get_service_random_number);
    mGetRandomNumberBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "AnotherBindServiceActivity is:"+this.toString());
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch(uiID){
      case R.id.another_bind_service_start_bind_service_activity:
        Intent launchBindServiceActivity = new Intent(this, BindServiceActivity.class);
        launchBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchBindServiceActivity);
        break;
      case R.id.another_bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.another_bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.another_bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

重點在於AnotherBindServiceActivity也可綁定BindService,目前有BindServiceActivity和AnotherBindServiceActivity可綁定BindService。
另外修改BindServiceActivity新增按鍵用來綁定和解綁定以及啟動AnotherBindServiceActivity,如下

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumberBtn;
  private Button mtBindServiceBtn;
  private Button mtUnBindServiceBtn;
  private Button mStartAnotherBindServiceActivityBtn;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceConnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumberBtn = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumberBtn.setOnClickListener(this);
    mtBindServiceBtn = findViewById(R.id.bind_service_bind_service);
    mtBindServiceBtn.setOnClickListener(this);
    mtUnBindServiceBtn = findViewById(R.id.bind_service_unbind_service);
    mtUnBindServiceBtn.setOnClickListener(this);
    mStartAnotherBindServiceActivityBtn = findViewById(R.id.bind_service_start_another_bind_service_activity);
    mStartAnotherBindServiceActivityBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "BindServiceActivity is:"+this.toString());
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
      case R.id.bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.bind_service_start_another_bind_service_activity:
        Intent launchAnotherBindServiceActivity = new Intent(this, AnotherBindServiceActivity.class);
        launchAnotherBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchAnotherBindServiceActivity);
        break;
    }
  }
}

注意BindServiceActivity和AnotherBindServiceActivity是透過FLAG_ACTIVITY_REORDER_TOFRONT來互相切換,並不是產生新的Activity實體。
現在先啟動BindServiceActivity並點擊bind service按鈕,Log如下

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

可以看出服務已被綁定,接著點擊按鈕啟動AnotherBindServiceActivity並在該Activity中點擊bind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call bindService:
D/AnotherBindServiceActivity: onServiceConnected:

AnotherBindServiceActivity綁定服務之後服務不會再呼叫建構式以及onCreate, onBind方法,因為該服務已啟動。
接著點擊AnotherBindServiceActivity的unbind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call unbindService:

可以看到該服務並未銷毀,只是和AnotherBindServiceActivity解綁定。
接著按下start BindServiceActivity按鈕來啟動BindServiceActivity,啟動之後點擊unbind service按鈕,Log如下。

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

這時候因為所有和該服務綁定的元件都解綁定服務,服務銷毀。
 

綁定型服務的優缺點

綁定型服務的優點在於可以和多個元件互動,傳遞訊息。
缺點在於服務本身的執行也是在UI Thread中,沒有另外開啟新的執行緒來執行
 

分類
Android

授權處理

授權處理是什麼?

在某些情況下app對裝置的操作會影響隱私以及安全性等等,因此在app進行這些操作之前必須詢問使用者是否可以提供進行這些操作的權限。
這些權限必須宣告在AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
…
  <uses-permission android:name="android.permission.CAMERA"/>
…
</manifest>

要求權限的運作方式會根據app的targetSdkVersion和裝置的API level有不同的動作。
a.若app的targetSdkVersion大於等於23,且裝置的API level也是大於等於23。要求權限的動作會在app運作時,因此必須在執行這些權限前檢查是否已獲得授權,若已獲得授權就可執行相關動作,若未獲得授權可以要求使用者給予授權。
以上的機制也稱執行期權限檢查,也就是在運行app時才要求權限,安裝時不會要求授權。
b.若app的targetSdkVersion小於等於22,或裝置的API level也是小於等於22,要求權限的動作會在安裝app時請求授權,若使用者拒絕授權,app就無法安裝。若使用者給予授權,該授權就無法取消,除非重新安裝app。
以上的機制也稱安裝期權限檢查,不需進行處理,因為不給予授權即無法安裝app。
 

檢查是否已獲得授權

使用ContextCompat.checkSelfPermission方法

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
}

第1個參數為context,第2個參數為要檢查的授權,若方法回傳值等於PackageManager.PERMISSION_GRANTED 代表已獲得授權,方法回傳值等於PackageManager.PERMISSION_DENIED 代表未獲得授權。

要求申請授權

使用ActivityCompat.requestPermissions方法

ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);

第1個參數為Activity,第2個參數為要申請的授權,第3個參數為要求授權碼,該要求授權碼在”處理申請授權結果”需要用到。
呼叫requestPermission之後會顯示對話框讓使用者選擇是否給予授權,使用者選擇完畢後會觸發onRequestPermissionsResult方法,因此在Activity也需要複寫該方法。
 

處理申請授權結果

在Activity複寫onRequestPermissionsResult 方法,並在該方法中處理使用者回復申請授權結果。

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  if (requestCode == REQUEST_PERMISSION_CODE) {
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        //使用者給予授權後進行動作
    } else {
        //使用者拒絕給予授權進行動作
    }
  }
}

if (requestCode == REQUEST_PERMISSION_CODE)
首先檢查要求授權碼是否相符,因為可能在多個位置呼叫請授權的動作。
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
代表使用者給予授權
 

處理使用者點擊Deny & don’t ask again

若使用者在app第1次申請權限時拒絕給予授權,則第2次申請授權時會另外出現Deny & don’t ask again選項,若使用者點擊該選項,即使之後再要求授權都不會出現申請授權的對話框。
也就是說呼叫requestPermissions方法不會顯示申請授權的對話框,但還是會觸發onRequestPermissionsResult方法且該方法的第3個參數grantResults會回傳-1,-1代表使用者拒絕給予授權。
此時已無法再申請授權,除了重新安裝app以外就是使用者手動打開權限設定的頁面才能申請授權。
因此可在onRequestPermissionsResult方法內呼叫shouldShowRequestPermissionRationale 方法。
若使用者點擊Deny & don’t ask again 則 shouldShowRequestPermissionRationale方法回傳false,此時可以建立對話框告知使用者是否開啟授權頁面,是的話導引使用者開啟授權頁面,否的話告知使用者無法進行後續動作。

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
    @NonNull int[] grantResults) {
  if (requestCode == REQUEST_PERMISSION_CODE) {
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      startCameraActivity();
    } else {
      if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission.CAMERA)) {
        AlertDialog.Builder builder = new Builder(this);
        builder.setTitle("Warning").setMessage("需要開啟相機權限,是否開啟權限頁面?").setPositiveButton("開啟權限頁面",
            new OnClickListener() {
              @Override
              public void onClick(DialogInterface dialogInterface, int i) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivity(intent);
              }
            }).setNegativeButton("取消", null).show();
      } else {
        Toast.makeText(this, "需要授權以開啟相機", Toast.LENGTH_SHORT).show();
        Log.d(TAG, "onRequestPermissionsResult: user not click deny && don't ask");
      }
    }
  }
}

 
以上完成後可在該頁面加入按鈕讓使用者點擊後詢問是否開啟權限設定頁面

@Override
public void onClick(View view) {
  int uiID = view.getId();
  switch(uiID){
    case R.id.apply_permission:
      ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
      break;
  }
}

 

重點總結

1.在AndroidManifest.xml宣告需要的授權
2. 檢查是否已獲得授權
已獲得授權->執行授權相關動作
未獲得授權->進入3.要求申請授權
3.要求申請授權
直接進入4.處理申請授權結果
4.處理申請授權結果
使用者給予授權->執行授權相關動作
使用者未給予授權->提示使用者給予授權,並提供再度進行申請授權的動作
完整程式碼如下

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  private static final String TAG = MainActivity.class.getSimpleName();
  private static final int REQUEST_PERMISSION_CODE = 999;
  private Button mApplyPermissio;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mApplyPermissio = findViewById(R.id.apply_permission);
    mApplyPermissio.setOnClickListener(this);
    checkPermission();
  }
  private void checkPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
      startCameraActivity();
    } else {
      ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
    }
  }
  @Override
  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
      @NonNull int[] grantResults) {
    if (requestCode == REQUEST_PERMISSION_CODE) {
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        startCameraActivity();
      } else {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission.CAMERA)) {
          showOpenAppPermissionDialog();
        } else {
          Toast.makeText(this, "需要授權以開啟相機", Toast.LENGTH_SHORT).show();
        }
      }
    }
  }
  private void showOpenAppPermissionDialog() {
    Builder builder = new Builder(this);
    builder.setTitle("Warning").setMessage("需要開啟相機權限,是否開啟權限頁面?").setPositiveButton("開啟權限頁面",
        new OnClickListener() {
          @Override
          public void onClick(DialogInterface dialogInterface, int i) {
            Intent intent = new Intent();
            intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            Uri uri = Uri.fromParts("package", getPackageName(), null);
            intent.setData(uri);
            startActivity(intent);
          }
        }).setNegativeButton("取消", new OnClickListener() {
      @Override
      public void onClick(DialogInterface dialogInterface, int i) {
        Toast.makeText(MainActivity.this, "請開啟權限設定並給予授權", Toast.LENGTH_SHORT).show();
      }
    }).show();
  }
  private void startCameraActivity() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    startActivityForResult(intent, 0);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch(uiID){
      case R.id.apply_permission:
        ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
        break;
    }
  }
}

 
 
 
 
 

分類
Android

啟動型服務(IntentService)

IntentService是什麼?

IntentService是Service的一種,使用上較為方便簡單。其背景執行方式是透過 HandleThread,因此單個IntentService上的任務是循序執行的,可以保證執行緒安全。不同的IntentService在執行上也不會互相干擾。
IntentService適合用於客戶端不須和Service互動的情況下,基本上由客戶端呼叫startService並指定啟動的IntentService。


簡單的IntentService實作

public class SimpleIntentService extends IntentService {
  public SimpleIntentService() {
    super("SimpleIntentService");
  }
  @Override
  protected void onHandleIntent(Intent intent) {
  }
}

onHandleIntent方法會執行在新的執行緒上,也是放置耗時操作的位置。其參數intent就是從客戶端傳遞過來的intent。
另外需要在AndroidManifest.xml宣告該IntentService

<service
  android:exported="false"
  android:name=".simpleintentservice.SimpleIntentService">
</service>

android:exported=”false”代表是否可以由其他的App啟動。


啟動IntentService

客戶端透過呼叫startService方法來啟動IntentService

public class SimpleIntentServiceActivity extends AppCompatActivitye {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_simple_intent_service);
    Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
    startService(launchSimpleIntentService);
  }
}

客戶端傳遞資料到IntentService

從客戶端透過intent放置資料便可傳遞到IntentService

Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);

透過IntentService的onHandleIntent的參數取出資料

@Override
protected void onHandleIntent(Intent intent) {
  String data = intent.getStringExtra("data");
  Log.d(TAG, "data:" + data);
}

output:

D: data:data from activity

IntentService的生命週期

複寫IntentService的生命週期方法觀察呼叫順序

public class SimpleIntentService extends IntentService {
  private static final String TAG = SimpleIntentService.class.getSimpleName();
  public SimpleIntentService() {
    super("SimpleIntentService");
    Log.d(TAG, "SimpleIntentService: ");
  }
  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: ");
  }
  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroy: ");
  }
  @Override
  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "onHandleIntent: ");
    String data = intent.getStringExtra("data");
    Log.d(TAG, "data:" + data);
  }
}

Client call

Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);
Log.d(TAG, "startService");

Output

D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onDestroy:

從Output觀察IntentService生命週期的順序為
建構式 -> onCreate -> onStartCommand -> onHandleIntent -> onDestroy


客戶端停止IntentService

透過呼叫stopService()方法來停止IntentService。

Intent stopSimpleIntentService = new Intent(this, SimpleIntentService.class);
stopService(stopSimpleIntentService);
Log.d(TAG, "stopService");

傳入的Intent必須指定要停止的IntentService。
呼叫StopService之後,IntentService便會呼叫onDestroy方法銷毀自己。
但一般的使用情境較多為IntentService完成onHandleIntent方法後自動呼叫onDestroy銷毀的情況
需要注意的是因為onHandleIntent方法內部是由新的執行緒來執行,因此即使是客戶端呼叫了stopService,但是onHandleIntent方法不會結束。
為了測試這點在onHandleIntent方法加入計時10秒的操作

@Override
protected void onHandleIntent(Intent intent) {
  Log.d(TAG, "onHandleIntent: ");
  String data = intent.getStringExtra("data");
  Log.d(TAG, "data:" + data);
  sleep(10);
}
private void sleep(int sleepTimeSeconds) {
  for (int i = 0; i < sleepTimeSeconds; ++i) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    Log.d(TAG, "sleep: "+i);
  }
}

客戶端並在第3秒呼叫stopService
Output如下

D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0
D/SimpleIntentService: sleep: 1
D/SimpleIntentService: sleep: 2
D/SimpleIntentService: sleep: 3
D/SimpleIntentServiceActivity: stopService
D/SimpleIntentService: onDestroy:
D/SimpleIntentService: sleep: 4
D/SimpleIntentService: sleep: 5
D/SimpleIntentService: sleep: 6
D/SimpleIntentService: sleep: 7
D/SimpleIntentService: sleep: 8
D/SimpleIntentService: sleep: 9

可以看到即使IntentService已經銷毀了,但計時仍然繼續。
因此若要呼叫stopService也一併停止onHandleIntent的內容,可以建立成員變數來控制是否要停止計時,如下

private boolean mIsDestroy;
@Override
public void onDestroy() {
  mIsDestroy = true;
  super.onDestroy();
  Log.d(TAG, "onDestroy: ");
}
private void sleep(int sleepTimeSeconds) {
  for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    Log.d(TAG, "sleep: "+i);
  }
}

IntentService的循序

IntentService在單一執行緒上執行任務,若客戶端呼叫多次startService方法,IntentService也會保持等待當前的任務完成後再執行下一個任務。
修改印出耗時資訊時也印出IntentService toString

  private void sleep(int sleepTimeSeconds) {
    for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "sleep: "+i+" name:"+toString());
    }
  }

連續啟動3次IntentService,Outout如下

D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onDestroy:

可以看到每個任務都必須等到上一個任務完成後才執行。


不同IntentService的執行

觀察不同的IntentService執行的情況
新增另一個IntentService為AnotherSimpleIntentService如下

public class AnotherSimpleIntentService extends IntentService {
  private static final String TAG = AnotherSimpleIntentService.class.getSimpleName();
  private boolean mIsDestroy;
  public AnotherSimpleIntentService() {
    super("AnotherSimpleIntentService");
    Log.d(TAG, "AnotherSimpleIntentService: ");
  }
  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: ");
  }
  @Override
  public void onDestroy() {
    mIsDestroy = true;
    super.onDestroy();
    Log.d(TAG, "onDestroy: ");
  }
  @Override
  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "onHandleIntent: ");
    String data = intent.getStringExtra("data");
    Log.d(TAG, "data:" + data);
    sleep(10);
  }
  private void sleep(int sleepTimeSeconds) {
    for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "sleep: "+i+" name:"+toString());
    }
  }
}

AndroidManifest.xml宣告AnotherSimpleIntentService

    <service
      android:exported="false"
      android:name=".simpleintentservice.AnotherSimpleIntentService">
    </service>

在客戶端依序啟動SimpleIntentService和AnotherSimpleIntentService

  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.start_intent_service:
        startSimpleIntentService();
        startAnotherSimpleIntentService();
        break;
    }
  }
  private void startAnotherSimpleIntentService() {
    Intent launchAnotherSimpleIntentService = new Intent(this, AnotherSimpleIntentService.class);
    launchAnotherSimpleIntentService.putExtra("data", "data from activity");
    startService(launchAnotherSimpleIntentService);
    Log.d(TAG, "startService");
  }
  private void startSimpleIntentService() {
    Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
    launchSimpleIntentService.putExtra("data", "data from activity");
    startService(launchSimpleIntentService);
    Log.d(TAG, "startService");
  }

Output如下

D/SimpleIntentServiceActivity: startService
D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/AnotherSimpleIntentService: AnotherSimpleIntentService:
D/AnotherSimpleIntentService: onCreate:
D/AnotherSimpleIntentService: onStartCommand:
D/AnotherSimpleIntentService: onHandleIntent:
D/AnotherSimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/SimpleIntentService: onDestroy:
D/AnotherSimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/AnotherSimpleIntentService: onDestroy:

可以看到不同的IntentService彼此不會互相影響,各自執行任務。


 

IntentService的優缺點

IntentService的優點在於容易使用,保持循序。
缺點在於不容易與客戶端進行互動(可以透過內部類別來解決),無法並行。

分類
Android

Error: Default interface methods are only supported starting with Android N (–min-api 24)

這是在 Android Studio 中套用了 androidx.core 之後出現的問題,build project 出現以下錯誤:

Error: Default interface methods are only supported starting with Android N (--min-api 24): android.view.MenuItem androidx.core.internal.view.SupportMenuItem.setContentDescription(java.lang.CharSequence)

解決方法有 2 種:
1. 在 /app/build.gradle 修改 minSdkVersion 為 24

android {
    ...
    defaultConfig {
        ...
        minSdkVersion 24
        ...
    }
    ...
}

 
2.在 /app/build.gradle 加入以下內容

android {
   ...
    compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }
   ...
}

 

分類
Google Cloud Platform

Google Cloud Source Repositories(GCSR) 如何將遠端的 Repo clone 到本地端

Situation:

 想將 Google Cloud Source Repositories 已存在的遠端 repo clone 到本地端

 

Action : 

1.安裝 Google Cloud SDK(已安裝請忽略)

2.開啟 Google Cloud SDK Shell 並使用指令移動到想放置本地端 repo 的路徑

3.使用瀏覽器開啟 Cloud Source Repositories 的遠端 repo 頁面(以下為 hello-world 的示範頁面)

4.點擊右上方的”建立本機副本”並選擇 “Google Cloud SDK” 再複製”透過指令列複製存放區”的指令(這個指令就是把遠端 repo clone 到本地端的指令)

5.將第4步驟複製的指令貼到第2步驟開啟的 Google Cloud SDK Shell 中並執行(如下畫面)

 

Result : 

本地端應該可以看到從遠端 clone 下來的 repo)

分類
Android Architectural Pattern

Model in todo-mvp

概述

透過todo-mvp來說明MVP中的Model
todo-mvp 是 Android 官方用來說明 MVP Pattern的範例,參考 https://github.com/googlesamples/android-architecture
todo-mvp 裡的 Model 為TaskRepository,TaskRepository繼承TasksDataSource。
TaskDataSource實際上是一個interface,其中2個內部介面LoadTasksCallback和GetTasksCallback用來作callback使用,在內部介面的onTasksLoaded方法用來當取得task成功之後把task傳回呼叫點的用途,而onDataNotAvailable方法用來當取得task失敗後的後續處理。
其餘在TasksDataSource介面的方法都是存取資料的共用方法,只要是Model都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

接著看Presenter如何關聯 Model以及使用Model。把焦點放在AddEditTaskPresenter。
1.AddEditTaskPresenter(Presenter)本身不會持有任何資料,資料放在Model中。
2.Presenter會通知Model去改變資料。
3.Presenter會持有Model和View的變數並在建構式初始化他們。
4.Presenter會在建構式初始化Model,接著在需要改變資料的位置去操縱Model改變資料,Model改變資料後Presenter再通知View重新載入資料。
AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    private boolean mIsDataMissing;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     * @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes)
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask() && mIsDataMissing) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
        mIsDataMissing = false;
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    @Override
    public boolean isDataMissing() {
        return mIsDataMissing;
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

注意雖然Model的變數型態為TasksDataSource(interface),但在Presenter建構式傳入的其實是TaskRepository(繼承自TasksDataSource)。
AddEditTaskActivity.java

...
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
...

Injection.java

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

因此我們需要看的是TasksRepository的內容。
TasksRepository.java

public class TasksRepository implements TasksDataSource {
    private static TasksRepository INSTANCE = null;
    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;
    /**
     * This variable has package local visibility so it can be accessed from tests.
     */
    Map<String, Task> mCachedTasks;
    /**
     * Marks the cache as invalid, to force an update the next time data is requested. This variable
     * has package local visibility so it can be accessed from tests.
     */
    boolean mCacheIsDirty = false;
    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }
    /**
     * Returns the single instance of this class, creating it if necessary.
     *
     * @param tasksRemoteDataSource the backend data source
     * @param tasksLocalDataSource  the device storage data source
     * @return the {@link TasksRepository} instance
     */
    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }
    /**
     * Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
     * next time it's called.
     */
    public static void destroyInstance() {
        INSTANCE = null;
    }
    /**
     * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
     * available first.
     * <p>
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if all data sources fail to
     * get the data.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);
        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }
        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }
                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }
    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.completeTask(task);
        mTasksLocalDataSource.completeTask(task);
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        checkNotNull(taskId);
        completeTask(getTaskWithId(taskId));
    }
    @Override
    public void activateTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.activateTask(task);
        mTasksLocalDataSource.activateTask(task);
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        checkNotNull(taskId);
        activateTask(getTaskWithId(taskId));
    }
    @Override
    public void clearCompletedTasks() {
        mTasksRemoteDataSource.clearCompletedTasks();
        mTasksLocalDataSource.clearCompletedTasks();
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    /**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }
    @Override
    public void refreshTasks() {
        mCacheIsDirty = true;
    }
    @Override
    public void deleteAllTasks() {
        mTasksRemoteDataSource.deleteAllTasks();
        mTasksLocalDataSource.deleteAllTasks();
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        mTasksRemoteDataSource.deleteTask(checkNotNull(taskId));
        mTasksLocalDataSource.deleteTask(checkNotNull(taskId));
        mCachedTasks.remove(taskId);
    }
    private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
        mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                refreshLocalDataSource(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }
            @Override
            public void onDataNotAvailable() {
                callback.onDataNotAvailable();
            }
        });
    }
    private void refreshCache(List<Task> tasks) {
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
        for (Task task : tasks) {
            mCachedTasks.put(task.getId(), task);
        }
        mCacheIsDirty = false;
    }
    private void refreshLocalDataSource(List<Task> tasks) {
        mTasksLocalDataSource.deleteAllTasks();
        for (Task task : tasks) {
            mTasksLocalDataSource.saveTask(task);
        }
    }
    @Nullable
    private Task getTaskWithId(@NonNull String id) {
        checkNotNull(id);
        if (mCachedTasks == null || mCachedTasks.isEmpty()) {
            return null;
        } else {
            return mCachedTasks.get(id);
        }
    }
}

TasksRepository實現3層緩存,首先第1層緩存為記憶體也就是Map<String, Task> mCachedTasks;
第2層緩存為本地端資料來源,也就是private final TasksDataSource mTasksLocalDataSource;
因為該變數的型態也是TasksDataSource為interface,因此不會被資料來源的實現綁住,也就是說若想更換不同的資料庫,也只要新增TasksDataSource的子類別繼承TasksDataSource即可(OCP)。
第3層緩存為遠端資料來源,為 private final TasksDataSource mTasksRemoteDataSource;
變數型態也是TasksDataSource,也可以簡單替換遠端來源的實現(OCP),如volley, okhttp, retrofit等等。
若以儲存資料來說,在順序性來說沒有分別,這3層都會儲存資料,如下面的TasksRepository.saveTask方法的實作內容

    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }

若是讀取資料,則會先從第1層緩存記憶體(mCachedTasks)去讀取資料,若資料存在就直接回傳,若資料不存在,則從第2層緩存本地端資料庫(mTasksLocalDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料。
若還是沒有資料則從第3層緩存遠端網路(mTasksRemoteDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料,若資料不存在則顯示該資料不存在訊息。
如下方的TasksRepository.getTask方法內容

    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

接著來看看在TasksRepository建構式,存取權限為私有,代表只能透過該纇別內部呼叫。

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }

呼叫該建構式的位置為getInstance()方法,會透過其方法的參數設定tasksRemoteDataSource和tasksLocalDataSource。

    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }

而getInstance方法的呼叫者為Injection類別的ProvideTasksRepository方法。

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}

可以看到provideTasksRepository方法內即為FakeTasksRemoteDataSource.getIntance()和TasksLocalDataSource.getInstance()分別代表遠端資料來源和本地端資料來源。
接著看看FakeTasksRemoteDataSource類別。其內容相當簡單,儲存資料的方式是透過一個Map<String, Task> TASKS_SERVICE_DATA 來儲存資料。
FakeTasksRemoteDataSource.java

public class FakeTasksRemoteDataSource implements TasksDataSource {
    private static FakeTasksRemoteDataSource INSTANCE;
    private static final Map<String, Task> TASKS_SERVICE_DATA = new LinkedHashMap<>();
    // Prevent direct instantiation.
    private FakeTasksRemoteDataSource() {}
    public static FakeTasksRemoteDataSource getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new FakeTasksRemoteDataSource();
        }
        return INSTANCE;
    }
    @Override
    public void getTasks(@NonNull LoadTasksCallback callback) {
        callback.onTasksLoaded(Lists.newArrayList(TASKS_SERVICE_DATA.values()));
    }
    @Override
    public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) {
        Task task = TASKS_SERVICE_DATA.get(taskId);
        callback.onTaskLoaded(task);
    }
    @Override
    public void saveTask(@NonNull Task task) {
        TASKS_SERVICE_DATA.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        TASKS_SERVICE_DATA.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void activateTask(@NonNull Task task) {
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        TASKS_SERVICE_DATA.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void clearCompletedTasks() {
        Iterator<Map.Entry<String, Task>> it = TASKS_SERVICE_DATA.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        TASKS_SERVICE_DATA.remove(taskId);
    }
    @Override
    public void deleteAllTasks() {
        TASKS_SERVICE_DATA.clear();
    }
    @VisibleForTesting
    public void addTasks(Task... tasks) {
        for (Task task : tasks) {
            TASKS_SERVICE_DATA.put(task.getId(), task);
        }
    }
}

 
最後看看 TasksLocalDataSource 類別。
該類別有TaskDao以及AppExecutors 變數,其中TaskDao提供存取Task的介面,為使用Room的寫法。關於Room可以參考這篇
而AppExecutors主要負責Executor的執行。
TasksLocalDataSource.java

public class TasksLocalDataSource implements TasksDataSource {
    private static volatile TasksLocalDataSource INSTANCE;
    private TasksDao mTasksDao;
    private AppExecutors mAppExecutors;
    // Prevent direct instantiation.
    private TasksLocalDataSource(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        mAppExecutors = appExecutors;
        mTasksDao = tasksDao;
    }
    public static TasksLocalDataSource getInstance(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        if (INSTANCE == null) {
            synchronized (TasksLocalDataSource.class) {
                if (INSTANCE == null) {
                    INSTANCE = new TasksLocalDataSource(appExecutors, tasksDao);
                }
            }
        }
        return INSTANCE;
    }
    /**
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if the database doesn't exist
     * or the table is empty.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final List<Task> tasks = mTasksDao.getTasks();
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (tasks.isEmpty()) {
                            // This will be called if the table is new or just empty.
                            callback.onDataNotAvailable();
                        } else {
                            callback.onTasksLoaded(tasks);
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    /**
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if the {@link Task} isn't
     * found.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final Task task = mTasksDao.getTaskById(taskId);
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (task != null) {
                            callback.onTaskLoaded(task);
                        } else {
                            callback.onDataNotAvailable();
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    @Override
    public void saveTask(@NonNull final Task task) {
        checkNotNull(task);
        Runnable saveRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.insertTask(task);
            }
        };
        mAppExecutors.diskIO().execute(saveRunnable);
    }
    @Override
    public void completeTask(@NonNull final Task task) {
        Runnable completeRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), true);
            }
        };
        mAppExecutors.diskIO().execute(completeRunnable);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void activateTask(@NonNull final Task task) {
        Runnable activateRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), false);
            }
        };
        mAppExecutors.diskIO().execute(activateRunnable);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void clearCompletedTasks() {
        Runnable clearTasksRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteCompletedTasks();
            }
        };
        mAppExecutors.diskIO().execute(clearTasksRunnable);
    }
    @Override
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteAllTasks() {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTasks();
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @Override
    public void deleteTask(@NonNull final String taskId) {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTaskById(taskId);
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @VisibleForTesting
    static void clearInstance() {
        INSTANCE = null;
    }
}

整體來說因為Model提供了三層緩存,複雜度反而比Presenter和View還高。
從裡面的設計也可以看到設計原則(OCP)等等。
以上便是 todo-mvp 的 Model 。


以下為MVP相關內容

MVP Pattern in Android

分類
Android Uncategorized

如何從 adb 啟動 App 並帶參數

概述

如何從adb啟動App並帶參數

做法

以todo-app為例,使用adb啟動App使用的指令為

adb shell am start -n [PACKAGE-NAME]/[ACTIVITY-NAME]

因此需要先找到PACKAGE-NAME 和 ACTIVITY-NAME

1.找PACKAGE-NAME

先安裝 todo App 到裝置上
1.1輸入以下指令便會列出 App 上所有已安裝的 PACKAGE-NAME

adb shell pm list packages -f

若連接多台裝置則使用 -s 指定裝置號碼如下

adb -s DeviceNumber shell pm listpackages -f

如何取得裝置號碼則使用 adb devices
輸入adb shell pm list packages -f 之後回傳的內容可能太長,因此可以在指令的最後加上 > D:\testlog\get_packages.txt 將顯示內容輸出到D:\testlog\get_packages.txt
因此輸入

adb -s DeviceNumber shell pm list packages -f > D:\testlog\get_packages.txt

在d:\testlog\get_packages.txt尋找todo關鍵字,只找到一項如下

package:/data/app/com.example.android.architecture.blueprints.todomvp.mock-1/base.apk=com.example.android.architecture.blueprints.todomvp.mock

 

2.找ACTIVITY-NAME

輸入 adb shell dumpsys activity 便可列出正在 App 上執行的所有 Activity
注意,App必須正在執行中才會顯示 activity name
2.1先在裝置上啟動 todo-app
2.2 輸入

adb -s DeviceNumber shell dumpsys activity -f > D:\testlog\get_activities.txt

2.3在D:\testlog\get_activities.txt 尋找 todo關鍵字,找到其中一個區塊如下

…
TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
        Hist #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
          ProcessRecord{66be5e 5459:com.example.android.architecture.blueprints.todomvp.mock/u0a566}
    Running activities (most recent first):
      TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
    mResumedActivity: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
…

在該區塊中尋找關鍵字cmp,可以找到以下內容

cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity

cmp=後方的內容其實就是PACKAGE-NAME/ACTIVITY-NAME
因此可以輸入下方指令來啟動todo App了

adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity

成功啟動便會回應

Starting: Intent { cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity }

並在裝置上啟動todo app。

3.啟動App附帶參數

啟動App附帶參數的方式為在指令的最後加入 –es key ‘value’ 或 –ez key ‘value’或 –ei key ‘value’ 或 –ef key ‘value’
其中 –es 參數為string, –ez 參數為boolean, –ei參數型態為int, –ef參數為float。
若要取得參數,則在該Activity的onCreate方法中使用 getIntent().getXXXExtra的方式,xxx則視傳入參數為甚麼型態而定,若現在想啟動todo app並帶參數為string型態,參數的key為test, 參數的value為 test parameter from adb
,則輸入指令如下

adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity --es test 'test parameter from adb'

 
在TasksActivity的onCreate方法中加入

Log.d(TAG, "onCreate: "+getIntent().getStringExtra("test"));

觀察logcat輸出便可看到 onCreate: test parameter from adb,代表使用adb成功啟動App並附帶參數。
 

分類
Android

使用 Stetho 查看實體或虛擬裝置的資料庫內容

概述

Stetho 為 Facebook 出品的開源 Android 調試工具(官網連結),主要功能有網路抓包,查看資料庫,查看視圖階層等等。
本篇主要描述如何使用 Stetho 查看實體裝置的資料庫內容。

步驟

1.Dependencies

在 Module 的 build.gradle 加入以下內容

dependencies {
…
    implementation 'com.facebook.stetho:stetho:1.5.0'
}

 

2.在 App 的 Source Code 初始化 Stetho

在 App 第一個啟動 Activity 的 onCreate 方法或新增一個類別繼承 Application 的 onCreate方法加入Stetho.initializeWithDefaults(this);

public class SingletonApplication extends Application {
        public void onCreate() {
        super.onCreate();
        Stetho.initializeWithDefaults(this);
    }
}

 

3.啟動 Stetho 工具

開啟 chrome 瀏覽器並輸入 chrome://inspect 就會開啟 Stetho 工具,Stetho 工具的用途為提供管理所有可調試元件的介面。在紅框內即為連接的實體裝置,虛擬裝置也行。

4.啟動 App

啟動 App 後可以在 Stetho 工具的 Remote Target 看到啟動的 App 名稱
以 todo-app 為例。

點擊 inspect 便會跳出 DevTools 網頁,DevTools 網頁為該 App 專屬的調試工具,點擊網頁上方的 Resources -> 左方 Web SQL 就會顯示該 App 所使用的 db 檔(資料庫),再展開 db 檔(資料庫),內容為即為該資料庫內的資料表。
 
以 todo-app 為例,下方為啟動 App 後一開始的資料庫和資料表範例。
Tasks.db 為資料庫,其中有 tasks 資料表,但因為目前還未新增資料因此資料表沒有內容。

接著在 App 中新增資料,完成後點擊 DevTools 下方有個刷新的圖示或是再次點擊 tasks,就會顯示剛剛新增資料的內容(如紅框)

以上為使用 Stetho 觀察實體裝置的資料庫內容方法。