Android 連結FCM(Firebase Cloud Messaging)

1. 摘要
2. 新增FCM流程
3. 寫一個FCM Service
4. 在AndroidManifest.xml內新增
5. Activity中獲取token

1. 摘要

將Android Studio與Firebase Cloud Messaging連結的簡易流程

2. 新增FCM流程

  1. 如果有Model必須將每個Model各別Connect to Firebase,直到顯示Connected
  2. 如果有Model必須將每個Model各別Add FCM to your app,直到顯示Dependencies set up correctly

3. 寫一個FCM Service

class FCMServer: FirebaseMessagingService() {
    companion object {
        var fcmToken = ""
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        fcmToken = token
        println("NEW_TOKEN $token")
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        if(remoteMessage.data.isNotEmpty()) {
        }
    }

    override fun onMessageSent(p0: String) {
        super.onMessageSent(p0)
    }
}

4. 在AndroidManifest.xml內新增

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

5. Activity中獲取token

private fun initFCM() {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
//Log.w(TAG, "Fetching FCM registration token failed", task.exception)
println("Fetching FCM registration token failed ${task.exception}")
return@OnCompleteListener
}
println("fcmToken ${task.result}")
// Get new FCM registration token
FCMServer.fcmToken = task.result
})
}

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android Navigation搭配BottomNavigationView

1. 摘要
2. 使用方法
3. BottomNavigationView取消內建顏色變化

1. 摘要

參考:Navigation使用官網Android Jetpack Navigation與Safe Args使用

Library引用方法,以及基本的設定,請參考Android Jetpack Navigation與Safe Args使用

這裡主要是介紹「Navigation搭配BottomNavigationView使用方法」

2. 使用方法

在Layout中新增

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?attr/colorPrimaryVariant"
app:labelVisibilityMode="unlabeled"
app:itemIconSize="@dimen/icon_45"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />

<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/local_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

onCreate新增

val navController = findNavController(activity, R.id.nav_host_fragment_activity_main)

定義AppBarConfiguration

以下的R.id是BottomNavigationView的menu,menu/bottom_nav_menu裡面的

val appBarConfiguration = AppBarConfiguration(setOf(
    R.id.navigation_home,
    R.id.navigation_manager,
    R.id.navigation_account,
    R.id.navigation_data
))

設定appBarConfiguration

setupActionBarWithNavController(navController, appBarConfiguration)
setupWithNavController(navController)

點擊BottomNavigationView監聽方式

navController.addOnDestinationChangedListener { _, destination, _ ->
    when (destination.id) {
        R.id.navigation_home -> {
            
        }
        R.id.navigation_manager -> {
            
        }
        R.id.navigation_account -> {
            
        }
        R.id.navigation_data -> {
            
        }
    }
}

3. BottomNavigationView取消內建顏色變化

bottomNavigationView.itemIconTintList = null

這個方法目前無法於xml中設定

設定後,BottomNavigationView Menu就可以使用比較複雜的icon


相關文章

Android Jetpack Navigation與Safe Args使用
1. 摘要
2. 新增Navigation依賴
3. 基本使用方法
4. 使用Navigation Safe Args

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android Jetpack Navigation與Safe Args使用

1. 摘要
2. 新增Navigation依賴
3. 基本使用方法
4. 使用Navigation Safe Args

1. 摘要

參考:Navigation使用官網Safe Args傳遞資料官網

使用Navigation 做Fragment之間的切換,可以更便利,更清楚誰與誰之間做導航

Safe Args 傳遞安全的數據,是官網強烈建議使用,當然也可以不使用,用原本的Bundle做傳遞

2. 新增Navigation依賴

在build.gradle中新增以下

dependencies {
    ...
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
}

3. 基本使用方法

在app/src/main/res新增Direction: navigation

在navigation資料夾上新增

navigation裡面新增以下程式碼

<navigation 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:id="@+id/navigation"
app:startDestination="@id/navHomeFragment">
<fragment
android:id="@+id/navHomeFragment"
android:name="com.example.myapplication.fragment.NavHomeFragment"
android:label="NavHomeFragment"
tools:layout="@layout/fragment_nav_home">
<action
android:id="@+id/action_navHomeFragment_to_navSecondFragment"
app:destination="@id/navSecondFragment" />
</fragment>
<fragment
android:id="@+id/navSecondFragment"
android:name="com.example.myapplication.fragment.NavSecondFragment"
android:label="NavSecondFragment"
tools:layout="@layout/fragment_nav_second"/>
</navigation>

startDestination是起始的fragment
action是拉線後自動會新增的,看id名字很明顯可以知道是哪導航到哪

在Activity的layout中新增以下程式碼

<fragment
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:navGraph="@navigation/navigation"/>

navGraph是剛剛新增的navigation.xml
androidx.navigation.fragment.NavHostFragment是固定的別打錯
app:defaultNavHost="true" 屬性確保您的 NavHostFragment 會攔截系統返回按鈕

Activity onCreate新增Nav

lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_navigation)
    navController = Navigation.findNavController(this, R.id.fragment)
    NavigationUI.setupActionBarWithNavController(this, navController)
}

override fun onSupportNavigateUp(): Boolean {
    //按back返回的方法
    return navController.navigateUp()
}

Fragment 更換介面方式

下面的id是先前那個action的id

可以帶入bundle,也可以不帶入

val bundle = bundleOf(
"name" to firstName,
"mobile" to mobile.toLong()
)

findNavController().navigate(
R.id.action_enterDetailsFragment_to_verifyDetailsFragment,
bundle
)

讀取bundle方式,在目的地的Fragment onCreateView,新增以下程式碼

val name = arguments?.getString("name")
val mobileNumber = arguments?.getLong("mobile")

4. 使用Navigation Safe Args

官方強烈建議您將 Safe Args 用於導航和數據傳遞,因為它可以確保類型安全

新增依賴

在Project build.gradle新增以下

dependencies {
    def nav_version = "2.3.5"
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}

在app build.gradle新增以下

plugins {
    id 'androidx.navigation.safeargs.kotlin'
}

新增完依賴後

Build -> clean Project

Build -> Rebuild Project

選擇Android
在Java(generated)會出現這張圖的內容

Android Studio 4.1以上版本,如果navigation已建立好,並新增上述依賴,如果沒出現的話

在app build.gradle內新增以下內容

android {
    sourceSets {
        getByName("main").java.srcDirs("build/generated/source/navigation-args")
    }
}

新增好後

Build -> clean Project

Build -> Rebuild Project

此時會在Java內出現這張圖的內容

如果有出現就是成功添加囉

使用方法

原本是以下程式碼

val bundle = bundleOf(
    "name" to firstName,
    "mobile" to mobile.toLong()
)
findNavController().navigate(
    R.id.action_enterDetailsFragment_to_verifyDetailsFragment,
    bundle
)

使用safe args時,至navigation VerifyDetailsFragment 新增argument

<fragment xxxxx>
    <argument android:name="name"
        app:argType="string"/>
    <argument android:name="mobile"
        app:argType="long"/>
</fragment>

新增好後Build -> Make Project

就可以改用下方用法囉

findNavController().navigate(
EnterDetailsFragmentDirections.actionEnterDetailsFragmentToVerifyDetailsFragment(
        firstName,
        mobile.toLong()
    )
)

讀取方法如下

原本讀取方法

val name = arguments?.getString("name")
val mobileNumber = arguments?.getLong("mobile")

變更為以下方法

val args: VerifyDetailsFragmentArgs by navArgs()
args.let {
println("name ${it.name}, mobile ${it.mobile}")
}

這就是使用Safe Args的方法


相關文章

Android Navigation搭配BottomNavigationView
1. 摘要
2. 使用方法
3. BottomNavigationView取消內建顏色變化

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android 應用中使用Dagger-2

簡要

看此篇前

前篇需先看完,因為是接續的Android 應用中使用Dagger-1

如果還不知道Dagger是什麼,參考這篇Android Dagger基本知識與簡單測試

如果因為@Inject lateinit這行編譯錯誤,參考Android Dagger @Inject lateinit編譯錯誤?

1. Dagger 子组件

上一章節Android 應用中使用Dagger-1的LoginViewModel流程結束後,依然會存在內存內

如果希望LoginViewModel作用域限定為LoginActivity生命週期

首先先新增一個容器

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

這時有個問題,LoginComponent必須要可以訪問ApplicationComponent

因為LoginViewModel依賴於UserRepository

父物件中提供的所有對象,也會在子物件中提供

如需創建子物件實例,必須要父物件實例

因此在此必須將 LoginComponent 定義為 ApplicationComponent 的子組件

@Subcomponent 為 LoginComponent 添加註釋

@Subcomponent
interface LoginComponent {
fun inject(activity: LoginActivity)
}

還必須在 LoginComponent 內部定義子組件工廠

以便 ApplicationComponent 知道如何創建 LoginComponent 的實例

@Subcomponent
interface LoginComponent {
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(activity: LoginActivity)
}

創建新的Dagger 模塊(例如 SubcomponentsModule),將子組件的類傳遞給屬性註釋的子組件

@Module(subcomponents = [LoginComponent::class])
class SubcomponentsModule {
}

將新模塊(即SubcomponentsModule)添加到ApplicationComponent

並提供LoginComponent 實例的 factory

@Singleton
@Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
    //已刪除fun inject(activity: LoginActivity)
    //提供LoginComponent 實例的 factory
    fun loginComponent(): LoginComponent.Factory
    fun getInt(): Int
}

請注意,ApplicationComponent 不再需要注入 LoginActivity,因為現在由 LoginComponent 負責注入,因此您可以從 ApplicationComponent 中移除 inject() 方法

2. 為子組件分配作用域

ApplicationComponent按應用的生命週期,基本上都是相同實例

LoginComponent對於每個Activity都使用新的實例,Fragment中使用相同實例

注意:作用域限定規則如下:

  1. 如果某個類型標記有作用域註釋,該類型就只能由帶有相同作用域註釋的組件使用。
  2. 如果某個組件標記有作用域註釋,該組件就只能提供帶有該註釋的類型或不帶註釋的類型。
  3. 子組件不能使用其某一父組件使用的作用域註釋。

組件還涉及此上下文中的子組件

在Activity中新增

lateinit var loginComponent: LoginComponent

Activity onCreate中新增

override fun onCreate(
        savedInstanceState: Bundle?
    ) {
        println("result: ${(applicationContext as MyApplication)
            .appComponent.getInt()}")
        //注入Activity
        loginComponent = (applicationContext as MyApplication)
            .appComponent.loginComponent().create()
        loginComponent.inject(this)
        //現在loginViewModel是可靠的,可使用
        super.onCreate(savedInstanceState)
}

每次請求時,LoginComponent 必須始終提供 LoginViewModel 的同一實例

@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository
) {
fun testFunction() {
println("into testFunction")
}
}
@ActivityScope
@Subcomponent
interface LoginComponent {
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(activity: LoginActivity)
}

如果您有兩個需要 LoginViewModel 的 Fragment,系統就會為它們提供同一實例。

例如,如果您有 LoginUsernameFragment 和 LoginPasswordFragment,它們需要由 LoginComponent 注入

class LoginUsernameFragment: Fragment() {
@Inject
lateinit var loginViewModel: LoginViewModel

override fun onAttach(context: Context) {
super.onAttach(context)
(activity as LoginActivity).loginComponent.inject(this)
}
}
class LoginPasswordFragment: Fragment() {
@Inject
lateinit var loginViewModel: LoginViewModel

override fun onAttach(context: Context) {
super.onAttach(context)
(activity as LoginActivity).loginComponent.inject(this)
}
}
@ActivityScope
@Subcomponent
interface LoginComponent {
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(activity: LoginActivity)
fun inject(usernameFragment: LoginUsernameFragment)
fun inject(passwordFragment: LoginPasswordFragment)
}
新的Dagger圖長這樣

下面我們詳細介紹該圖的各個部分:

  1. NetworkModule(以及由此產生的 LoginRetrofitService)包含在 ApplicationComponent 中,因為您在組件中指定了它。
  2. UserRepository 保留在 ApplicationComponent 中,因為其作用域限定為 ApplicationComponent。如果項目擴大,您會希望跨不同功能(例如註冊)共享同一實例。
    由於 UserRepository 是 ApplicationComponent 的一部分,其依賴項(即 UserLocalDataSource 和 UserRemoteDataSource)也必須位於此組件中,以便能夠提供 UserRepository 的實例。
  3. LoginViewModel 包含在 LoginComponent 中,因為只有 LoginComponent 注入的類才需要它。 LoginViewModel 未包含在 ApplicationComponent 中,因為 ApplicationComponent 中的任何依賴項都不需要 LoginViewModel。

除了將對像作用域限定為不同的生命週期之外,創建子組件是分別封裝應用的不同部分的良好做法

根據應用流程構建應用以創建不同的 Dagger 子圖有助於在內存和啟動時間方面實現性能和擴容性更強的應用

注意:如果您需要使容器在出現設備旋轉等配置更改後繼續存在,請遵循保存界面狀態指南。您可能需要採用與處理進程終止相同的方式處理配置更改;否則,您的應用可能會在低端設備上丟失狀態。

3. 構建 Dagger 圖的最佳做法

為應用構建 Dagger 圖時:

  • 創建組件時,應該考慮什麼元素會決定該組件的生命週期。在本示例中,應用類負責 ApplicationComponent,而 LoginActivity 負責 LoginComponent。
  • 請僅在必要時使用作用域限定。過度使用作用域限定可能會對應用的運行時性能產生負面影響只要組件在內存中,對象就會在內存中獲取限定作用域的對象的成本更高。當 Dagger 提供對象時,它使用 DoubleCheck 鎖定,而不是 factory 類型提供程序。

4. 使用 Dagger 模塊

良好做法是模塊只在組件中聲明一次,特定高級 Dagger 用例除外

假設圖的配置方式如下。 ApplicationComponent 包括 Module1 和 Module2,Module1 包括 ModuleX

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

如果 Module2 現在依賴於 ModuleX 提供的類。錯誤做法是將 ModuleX 包含在 Module2 中,因為這樣 ModuleX 在圖中就出現了兩次,如以下代碼段所示:

//此處為不好的做法
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

您應改為執行以下某項操作:

  1. 重構模塊,並將共同模塊提取到組件中。
  2. 使用兩個模塊共享的對象創建一個新模塊,並將其提取到組件中。

如果不以這種方式進行重構,就會導致許多模塊相互包含而沒有清晰的結構,並且更難以了解每個依賴項的來源

良好做法(選項 1):在 Dagger 圖中聲明一次 ModuleX

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

良好做法(選項 2):將 ModuleX 中 Module1 和 Module2 的共同依賴項提取到包含在該組件中的名為 ModuleXCommon 的新模塊。

然後,使用特定於每個模塊的依賴項創建名為 ModuleXWithModule1Dependencies 和 ModuleXWithModule2Dependencies 的另外兩個模塊。

所有模塊在 Dagger 圖中都只聲明一次。

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

以上內容參考Android 官網


相關文章

Android 依賴項注入(Dependency injection)Android Dagger基本知識與簡單測試
簡要
1. 非依賴項注入 vs 依賴項注入
2. 自動依賴項注入
簡要
1. Android使用Dagger前置作業
2. 基本使用方法
Android 應用中使用Dagger-1
簡要
建構方法

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android 應用中使用Dagger-1

簡要

看此篇前

如果還不知道Dagger是什麼,參考這篇Android Dagger基本知識與簡單測試

如果因為@Inject lateinit這行編譯錯誤,參考Android Dagger @Inject lateinit編譯錯誤?


最佳做法摘要

  • 如果有可能,請通過 @Inject 進行構造函數注入,以向 Dagger 圖中添加類型。如果沒有可能,請執行以下操作:
    • 使用 @Binds 告知 Dagger 接口應採用哪種實現
    • 使用 @Provides 告知 Dagger 如何提供您的項目所不具備的類
  • 您只能在組件中聲明一次模塊
  • 根據註釋的使用生命週期,為作用域註釋命名。示例包括 @ApplicationScope、@LoggedUserScope 和 @ActivityScope

備註:

  1. 對於 Activity,任何初始化代碼都需要放入 onCreate() 方法中。在類的構造函數中使用 @Inject 註釋(構造函數注入)。必須改為使用字段注入
  2. 字段注入只能在無法使用構造函數注入的 Android 框架類中使用

不知道字段注入是什麼,參考這篇Android 依賴項注入(Dependency injection)

建構方法

假設一個架構如下

首先先創建一個應用類(Application)的Dagger圖

只要運行狀態,就會存在內存內,優勢可以供給其他Android框架使用

它還允許您在測試中使用自定義應用類,從而簡化了測試

先新增一個字定義作用域ApplicationScope

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ApplicationScope

再來新增ApplicationComponent

由上面摘要提的,Activity任何初始化都必須放在onCreate,因此必需改為使用字段注入

所以符合無法使用構造函數注入,因此可以使用以下方法

@Component
interface ApplicationComponent {
fun inject(activity: LoginActivity)
}

接著新增以下程式碼,注意LoginViewModel只是常規類,不是 Android 架构组件 ViewModel

class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) {  
    fun testFunction() {
        println("into testFunction")
    }
}

再來新增把Activity注入,如下程式碼

class LoginActivity: AppCompatActivity() {
    @Inject
    lateinit var loginViewModel: LoginViewModel
    override fun onCreate(
        savedInstanceState: Bundle?
    ) {
        //注入Activity
        (applicationContext as MyApplication)
            .appComponent.inject(this)
        //現在loginViewModel是可靠的,可使用
        loginViewModel.testFunction()
        super.onCreate(savedInstanceState)
    }
}

上面程式碼運行後,會打印 into testFunction

新增一個Model,並使用

@Module
class NetworkModule {
@Provides
fun provideInt(): Int {
return 100
}
}

再來把ApplicationComponent新增modules

@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)

    //這個會對應到provideInt
    fun getInt(): Int
}

在onCreate內新增以下,就會打印 result: 100

println("result: ${(applicationContext as MyApplication)
.appComponent.getInt()}")

這樣就把Module設定好囉!

Dagger作用域

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
fun inject(activity: LoginActivity)
fun getInt(): Int
}
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) {

}

@Module
class NetworkModule {
    @Singleton
    @Provides
    fun provideInt(): Int {
        return 100
    }
}

如果使用此作用域,記得ApplicationComponent是應用類創建的

因此UserRepository會始終保持在內存,直到應用被銷毀

注意:

  • 使用作用域注释的模块,只能在带有相同作用域注释的组件中使用
  • 使用構造函數注入(通過@Inject)時,應在類中添加作用域註釋
    使用Dagger模塊時,應在@Provides方法中添加作用域註釋,如上面範例的Module
完成到此的架構圖

以上內容參考Android 官網


相關文章

Android 依賴項注入(Dependency injection)Android Dagger基本知識與簡單測試
簡要
1. 非依賴項注入 vs 依賴項注入
2. 自動依賴項注入
簡要
1. Android使用Dagger前置作業
2. 基本使用方法
Android 應用中使用Dagger-2
1. Dagger 子组件
2. 為子組件分配作用域
3. 構建 Dagger 圖的最佳做法
4. 使用 Dagger 模塊

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android Dagger基本知識與簡單測試

簡要

看此篇前

如果還不清楚依賴項注入是什麼,可以參考這篇Android 依賴項注入(Dependency injection)

如果不知道Android怎麼測試資料,參考這篇如何使用Android Studio 測試資料?


Android 應用中手動依賴項注入或服務器可能會出現問題,具體實現項目的大小

Dagger 會自動生成代碼,因為該代碼是在編譯時生成的,因此具有可緩性,而且性能要取決於其他問題的解決方案

注意:使用 Hilt 可在 Android 上實現依賴注入。Hilt 在 Dagger 的基礎上構建,提供了一種將 Dagger 依賴項注入 Android 應用的標準方法。

在構建時,Dagger 會走查您的代碼,並執行以下操作:

  • 構建並驗證依賴關係圖,確保:

・每個對象的依賴關係都可以得到滿足,從而避免出現運行時異常。
・不存在任何依賴循環,從而避免出現無限循環。

  • 生成在運行時用於創建實際對象及其依賴項的類。

備註:@Component自動生成的容器前面會加上Dagger

如果找不到,Android Studio先Rebuild Project

因為Dagger是自動生成,發生編譯錯誤,做一次這個操作,基本上就正常了

1. Android使用Dagger前置作業

打開app build.gradle,頂端新增

plugins {
    id 'kotlin-kapt'
}

下方新增

dependencies {

    implementation 'com.google.dagger:dagger:2.33'
    kapt 'com.google.dagger:dagger-compiler:2.33'
}

新增完後記得Rebuild Project,否則可能會發生無法使用問題

2. 基本使用方法

這裡主要先把基礎打好,否則後面會越看越看不懂

讓Dagger知道如何創建實例

首先先新增UserLocalDataSource、UserRemoteDataSource這兩個class

class UserLocalDataSource @Inject constructor() {

}
class UserRemoteDataSource @Inject constructor() {

}

注意這邊多了@Inject這個,是告訴Dagger如何創建實例

再來新增一個UserRepository

class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) {

}

一樣告訴Dagger如何創建UserRepository實例

因為UserLocalDataSource、UserRemoteDataSource已經放入了@Inject

所以Dagger也會知道如何創建他們的實例

創建容器,提供對象及各自依賴項

@Component
interface ApplicationGraph {
fun repository(): UserRepository
}

UserRepository在上面已經讓Dagger知道如何創建了

這裡在ApplicationGraph上面新增@Component

會創建一個容器,就像手動注入依賴項時的操作一樣

這樣建立好後,接著可以測試一下結果

@Test
fun test() {
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)
}

結果兩個userRepository是不相同的

Dagger作用域(Scoping with Dagger)

可以使用作用域註釋將某個對象的生命週期限定為其組件的生命週期。這意味著,每次需要提供該類型時,都會使用依賴項的同一實例

可以使用內建的@Singleton作用域的標示

也可以自定義作用域

使用方式在Dagger實例上與容器上面加入作用域標示,如下

//使用內建@Singleton標示作用域
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) {

}


//使用自定義作用域
//自定義作用域
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) {

}

作用域設定好後,再來測試一下

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

會發現兩個是相同實例囉!

備註:設定好作用域後,每次調用 applicationGraph.repository() 時,都會獲得 UserRepository 的同一實例

以上內容參考Android 官網


相關文章

Android 依賴項注入(Dependency injection)Android 應用中使用Dagger-1
簡要
1. 非依賴項注入 vs 依賴項注入
2. 自動依賴項注入
簡要
建構方法
Android 應用中使用Dagger-2
1. Dagger 子组件
2. 為子組件分配作用域
3. 構建 Dagger 圖的最佳做法
4. 使用 Dagger 模塊

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android 依賴項注入(Dependency injection)

簡要

依賴項注入簡稱:DI

遵循 DI 的原則可以為良好的應用架構奠定基礎

實現依賴項注入可為您帶來以下優勢:

重用代碼
易於重構
易於測試

1. 非依賴項注入 vs 依賴項注入

以下非依賴項注入Android官方範例

class Car {
    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}
效果圖

靈活性低,重用性低,測試不方便,需要重建Car才能更換engine

以下依賴項注入Android官方範例

//第一種構造函式注入
class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}


//第二種字段注入(或 setter 注入)
class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}
效果圖

靈活性高,重用性高,測試方便,不需要重建Car,就可以更新engine

以上這些皆為手動依賴項注入

手動依賴像注入缺點,類越多、依賴項越多、項目越大,操作起來越繁瑣,也可能會產生而外的問題

2. 自動依賴項注入

有一些庫通過自動執行創建和提供依賴項的過程解決此問題。它們歸為兩類:

  • 基於反射的解決方案,可在運行時連接依賴項。
  • 靜態解決方案,可生成在編譯時連接依賴項的代碼。

Dagger 是適用於 Java、Kotlin 和 Android 的熱門依賴項注入庫,由 Google 進行維護。 Dagger 為您創建和管理依賴關係圖,從而便於您在應用中使用 DI。它提供了完全靜態和編譯時依賴項,解決了基於反射的解決方案(如 Guice)的諸多開發和性能問題。

在 Android 應用中使用 Hilt

Hilt 是推薦用於在 Android 中實現依賴項注入的 Jetpack 庫。 Hilt 通過為項目中的每個 Android 類提供容器並自動為您管理其生命週期,定義了一種在應用中執行 DI 的標準方法。

Hilt 在熱門 DI 庫 Dagger 的基礎上構建而成,因而能夠受益於 Dagger 提供的編譯時正確性、運行時性能、可伸縮性和 Android Studio 支持。

之後會有幾篇文章介紹Dagger與Hilt

以上內容參考Android 官網


相關文章

Android Dagger基本知識與簡單測試Android 應用中使用Dagger-1
簡要
1. Android使用Dagger前置作業
2. 基本使用方法
簡要
建構方法
Android 應用中使用Dagger-2
1. Dagger 子组件
2. 為子組件分配作用域
3. 構建 Dagger 圖的最佳做法
4. 使用 Dagger 模塊

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android 對話框Dialogs

簡要

提示用戶做出決定或輸入更多訊息,不會佔據整個屏幕

避免直接對Dialog做實體化

應使用下列子類別之一:

  1. AlertDialog
  2. DatePickerDialog
  3. TimePickerDialog

注意:Android ProgressDialog已被棄用,因為會阻止用戶與應用進行互動,應遵循進展與活動設計準則,在布局中使用ProgressBar,而非ProgressDialog

可定義對話框的樣式和結構,應使用 DialogFragment 作為對話框

DialogFragment 類提供創建對話框和管理其外觀所需的所有控件,而調用 Dialog 對像上的方法

使用 DialogFragment 來管理對話框可確保對話框能正確處理各種生命週期事件,如用戶按“返回”按鈕或旋轉屏幕時

下列依序介紹

  1. 創建DialogFragment
  2. 建構提醒對話框
  3. 創建自定義佈局
  4. 將Activity作為Dialog使用
  5. DialogFragment事件傳回Activity
  6. 嵌入式Fragment

1. 創建DialogFragment

根據對話複雜度,進行各種回調方法,包括所有基本的Fragment生命週期

新增以下的程式碼,重新定義對話樣式及架構

class CustomDialogFragment: DialogFragment() {
    override fun onCreateDialog(
        savedInstanceState: Bundle?
    ): Dialog {
        return activity?.let {
            val builder = AlertDialog.Builder(it)
            builder
                //預設title樣式
                //不能和setCustomTitle共存
                //.setTitle("title A")
                //預設內容樣式
                //可以和setView共存
                //.setMessage("Test Message")
                .setCustomTitle(TextView(activity).also {
                    it.text = "title"

                })
                .setView(TextView(activity).also {
                    it.text = "content"
                })
                .setPositiveButton("OK") { dialog, id ->

                }
                .setNegativeButton("CANCEL") { dialog, id ->

                }
            builder.create()
        } ?: throw IllegalStateException(
            "Activity cannot be null"
        )
    }
}

呼叫方式如下

val customDialogFragment = CustomDialogFragment()
customDialogFragment.show(supportFragmentManager,"test")

2. 建構提醒對話框

提醒對話框有三個區塊

  1. 標題
  2. 內容區域
  3. 操作按紐

操作按鈕不應超過3個,根據Material Design解釋如下,參考Material Design

When there are three or more actions with long text labels in a Material dialog, products may use iOS action sheets or bottom sheets.

以下程式碼,基本框架的範例

private fun initDialog() {
    val builder: AlertDialog.Builder? = let {
        AlertDialog.Builder(it)
    }
    builder?.apply {
        setMessage("message")
        setTitle("title")
        setPositiveButton("OK"
        ) { dialog, id ->
            // User clicked OK button
        }
        setNegativeButton("CANCEL"
        ) { dialog, id ->
            // User cancelled the dialog
        }
    }
    val dialog: AlertDialog? = builder?.create()
    dialog?.show()
}
以上程式效果圖

添加列表

AlertDialog API提供三種列表

  • 傳統的單選列表
  • 永久性單選列表(單選按鈕)
  • 永久性多選列表(複選框)

在app/src/main/res/values新增array

<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="colors_array">
<item>RED</item>
<item>GREEN</item>
<item>BLUE</item>
</array>
</resources>

在DialogFragment新增以下程式碼,就實現了傳統的單選列表

也可以搭配ListAdapter,之後會有個文章介紹,這裡暫時不多介紹

override fun onCreateDialog(
    savedInstanceState: Bundle?
): Dialog {
    return activity?.let {
        val builder = AlertDialog.Builder(it)
        builder.setTitle("Colors")
            .setItems(R.array.colors_array
            ) { dialog, which ->
                //打印點選index
                println("which $which")
            }
        builder.create()
    } ?: throw IllegalStateException(
        "Activity cannot be null"
    )
}

永久性多選列表(複選框),程式碼如下

//預先已知Item Size
//可以保存所有Item狀態
var checkItems = BooleanArray(3)
override fun onCreateDialog(
    savedInstanceState: Bundle?
): Dialog {
    return activity?.let {
        //只保存true的狀態,其他皆不保存
        val selectedItems = ArrayList()
        val builder = AlertDialog.Builder(it)
        builder.setTitle("title")
            //checkItems如果不需要
            //可以設成null
            .setMultiChoiceItems(
                R.array.colors_array, 
                checkItems
            ) { dialog, which, isChecked ->
                if (isChecked) {
                    selectedItems.add(which)
                } else if (selectedItems.contains(which)) {
                    selectedItems.remove(which)
                }
            }
            .setPositiveButton("OK"
            ) { dialog, id ->
            }
            .setNegativeButton("CANCEL"
            ) { dialog, id ->
            }
        builder.create()
    } ?: throw IllegalStateException(
        "Activity cannot be null"
    )
}

永久性單選列表(單選按鈕),將上方程式碼,參照下方重點更換

就可以實現功能囉!

.setMultiChoiceItems(
    R.array.colors_array,
    checkItems
) { dialog, which, isChecked ->
//由上方程式更換成下方程式
.setSingleChoiceItems(
    R.array.colors_array,
    //預設選取index
    1
) { dialog, which ->

3. 創建自定義佈局

先建立一個xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="64dp"
android:scaleType="center"
android:background="#FFFFBB33"
android:contentDescription="@string/app_name" />
<EditText
android:id="@+id/username"
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="4dp"
android:hint="Username" />
<EditText
android:id="@+id/password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="16dp"
android:fontFamily="sans-serif"
android:hint="Password"/>
</LinearLayout>

默認情況下,當 EditText 使用 “textPassword" 輸入類型時
字體系列會設置為等寬
因此更改為 “sans-serif",以便兩個文本字段均使用匹配的字體樣式

override fun onCreateDialog(
    savedInstanceState: Bundle?
): Dialog {
    return activity?.let {
        val builder = AlertDialog.Builder(it)
        val inflater = requireActivity().layoutInflater;
        builder.setView(inflater.inflate(
            R.layout.dialog_custom, 
            null
        ))
            .setPositiveButton("Sign in"
            ) { dialog, id ->
            }
            .setNegativeButton("Cancel"
            ) { dialog, id ->
                getDialog()?.cancel()
            }
        builder.create()
    } ?: throw IllegalStateException(
        "Activity cannot be null"
    )
}
上方程式碼效果圖

4. 將Activity作為Dialog使用

首先先至style裡面新增以下,設定最小視窗大小

備註:如果沒設定運行大小會是最小化,可以自行測試看看

<style name="MainNoActionBarDialog" parent="android:Theme.Holo.Dialog">
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
<item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
</style>

再來打開AndroidManifest將Activity新增以下程式碼

<activity android:theme="@style/MainNoActionBarDialog">

接著把Activity繼承FragmentActivity()

在onCreate內在setContentView前,先增以下

//去除Title
requestWindowFeature(Window.FEATURE_NO_TITLE)

在幫Activity setContentView Layout

運行效果圖

5. DialogFragment事件傳回Activity

建立一個DialogFragment,如下

class CustomDialogFragment2: DialogFragment() {
    internal lateinit var listener: NoticeDialogListener
    override fun onCreateDialog(
        savedInstanceState: Bundle?
    ): Dialog {
        return activity?.let {
            val builder = AlertDialog.Builder(it)

            builder.setMessage("title")
                .setPositiveButton("ok"
                ) { dialog, id ->
                    listener.onDialogPositiveClick(this)
                }
                .setNegativeButton("cancel"
                ) { dialog, id ->
                    listener.onDialogNegativeClick(this)
                }

            builder.create()
        } ?: throw IllegalStateException(
            "Activity cannot be null"
        )
    }

    interface NoticeDialogListener {
        fun onDialogPositiveClick(dialog: DialogFragment)
        fun onDialogNegativeClick(dialog: DialogFragment)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = context as NoticeDialogListener
        } catch (e: ClassCastException) {
            throw ClassCastException((context.toString() +
                    " must implement NoticeDialogListener"))
        }
    }

}

Activity中,新增下方程式碼

fun showNoticeDialog() {
    val dialog = CustomDialogFragment2()
    //NoticeDialogFragment是唯一識別碼
    //可以透過findFragmentByTag()找到他
    dialog.show(
        supportFragmentManager,
        "NoticeDialogFragment"
    )
}
override fun onDialogPositiveClick(
    dialog: DialogFragment
) {
    println("onDialogPositiveClick")
}
override fun onDialogNegativeClick(
    dialog: DialogFragment
) {
    println("onDialogNegativeClick")
}

按下OK打印 onDialogPositiveClick

按下CANCEL打印 onDialogNegativeClick

6. 嵌入式Fragment

DialogFragment中新增以下程式碼

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    _binding = DialogCustomBinding
        .inflate(
            layoutInflater,
            container,
            false
        )
    return binding.root
}
override fun onCreateDialog(
    savedInstanceState: Bundle?
): Dialog {
    //要用super
    val dialog =
        super.onCreateDialog(savedInstanceState)
    dialog
        .requestWindowFeature(Window.FEATURE_NO_TITLE)
    return dialog
}

Activity中新增


//監聽按鈕,按下關閉嵌入式Fragment
binding.addBtn.setOnClickListener {
    val fragment = supportFragmentManager
        .findFragmentByTag("dialog") as? DialogFragment
    fragment?.dismiss()
}
//如果是大Layout則show
//如果不是則嵌入式Fragment
fun showDialog() {
    val fragmentManager = supportFragmentManager
    val newFragment = CustomDialogFragment()
    if (isLargeLayout) {
        newFragment.show(fragmentManager, "dialog")
    } else {
        val transaction = fragmentManager.beginTransaction()
        transaction.setTransition(
            FragmentTransaction.TRANSIT_FRAGMENT_OPEN
        )
        transaction
            .add(
                android.R.id.content, 
                newFragment,
                "dialog"
            )
            .addToBackStack(null)
            .commit()
    }
}

以上內容參考Android 官網


訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android Activity、Fragment應用欄(AppBar)使用

簡要

AppBar應用欄,提供固定的位置,用於顯示當前屏幕與操作

Activity擁有應用欄:Fragment變更/擴充菜單,用途每個介面皆需要應用欄,但Fragment的應用欄選項皆不同時,可以有效的讓各個介面有更好的規劃

Fragment擁有應用欄:大部分皆不需要應用欄,只有特定的Fragment需要時,會採用此方法

Menu的一些設置參考Android Menu showAsAction屬性

由ActionBar更換Toolbar,以及Toolbar一些設置參考Android ActionBar與Toolbar

以下分成幾個區塊介紹

  1. Activity擁有應用欄
  2. Fragment擁有應用欄
  3. 動態修改Menu是否顯示
  4. Menu點擊事件處理

1. Activity擁有應用欄

了解Menu基本的設定後,建立好的Menu該如何設定呢?


Activity擁有應用欄,意味著Activity與Fragment皆有Menu且是用共的,因為Activity擁有

用途:每個介面皆需要應用欄,但Fragment的應用欄選項皆不同時

首先先幫Activity與Fragment建立一個不一樣的Menu,方便觀察效果

Activity Menu如下

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/searchMenu"
        app:actionViewClass="android.widget.SearchView"
        android:icon="@drawable/ic_baseline_search_24"
        android:title="search"
        app:showAsAction="ifRoom|collapseActionView" />
    <item android:id="@+id/testMenu"
        android:icon="@drawable/ic_baseline_fast_forward_24"
        android:title="test2"
        app:showAsAction="ifRoom"/>
</menu>

Fragment Menu如下

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/backButton"
android:icon="@drawable/ic_baseline_chevron_left_24"
app:showAsAction="ifRoom"
android:title="test1"/>
<item android:id="@+id/testMenu2"
android:icon="@drawable/ic_baseline_favorite_24"
app:showAsAction="ifRoom"
android:title="test2"/>
</menu>

建立完後

Activity中新增下列程式碼

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.activity_menu, menu)
return true
}

Fragment中新增以下程式碼

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_menu, menu)
//切換到這個Fragment時,不需要使用searchMenu
menu.findItem(R.id.searchMenu).isVisible = false
}

在Fragment onCreated中新增,向Activity註冊使用

setHasOptionsMenu(true)

這樣在這個Fragment時,就有專屬的Menu選項,其他的頁面,皆顯示Activity的Menu

有設定專屬Menu的Fragment介面
沒設定Menu的Fragment,依照Activity Menu顯示

2. Fragment擁有應用欄

用途:大部分皆不需要應用欄,只有特定的Fragment需要時,會採用此方法

首先先把Activity的ActionBar隱藏

supportActionBar?.hide()
binding.mToolbar.visibility = View.GONE

隱藏後在Fragment的Layout新建

<androidx.appcompat.widget.Toolbar
android:id="@+id/mToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="title"
android:textSize="20dp"
android:textColor="@color/white"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>

接著在onViewCreated裡面新增下列程式碼,這樣就新增好Menu了

binding.mToolbar.inflateMenu(R.menu.fragment_menu)

如果要更換Menu方式也很簡單

這裡設定一個Button按下時變更Menu

binding.replaceMenuBtn.setOnClickListener {
    clearToolbarMenu()
    if(!isChangeMenu) {
        binding.mToolbar.inflateMenu(R.menu.activity_menu)
    } else {
        binding.mToolbar.inflateMenu(R.menu.fragment_menu)
    }
    isChangeMenu = !isChangeMenu
}

private fun clearToolbarMenu() {
    binding.mToolbar.menu.clear()
}

3. 動態修改Menu是否顯示

這裡分為兩部分

a. Activity擁有應用欄

在Fragment中,新增以下程式碼

private var isEditMenu = false
override fun onPrepareOptionsMenu(menu: Menu) {
    super.onPrepareOptionsMenu(menu)
    val item = menu.findItem(R.id.backButton)
    item.isVisible = isEditMenu
}

並設定一個Button,按下時,顯示/隱藏Menu

binding.replaceMenuBtn.setOnClickListener {
    isEditMenu = !isEditMenu
    requireActivity().invalidateOptionsMenu()
}

b. Fragment擁有應用欄

使用相對比較簡單一點,方式如下

在Fragment中,新增以下程式碼

private fun updateToolbar() {
    isEditMenu = !isEditMenu

    val saveItem = binding.mToolbar.menu.findItem(R.id.testMenu2)
    saveItem.isVisible = isEditMenu
}

設定一個Button,按下時,顯示/隱藏Menu

binding.backBtn.setOnClickListener {
updateToolbar()
}

4. Menu點擊事件處理

這裡分為兩部分

a. Activity擁有應用欄

在Fragment裡面新增下列程式碼

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.testMenu2 -> {
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}

如果是這個按鈕則攔截(設為true),否則不攔截給Activity處理(設為super.onOptionsItemSelected(item))

在Activity中新增下列程式碼

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when(item.itemId) {
        R.id.searchMenu -> {
            true
        }
        R.id.testMenu -> {
            true
        }
        R.id.testMenu2 -> {
            true
        }
        else -> super.onOptionsItemSelected(item)
    }
}

這樣設定完後,Fragment沒攔截的按鈕就可以在Activity中操作

b. Fragment擁有應用欄

監聽的方式與Activity擁有應用欄不太一樣,使用方式如下

binding.mToolbar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.testMenu2 -> {
println("test")
true
}
else -> false
}
}

以上內容參考Android 官網


相關文章

Android Menu showAsAction屬性Android ActionBar與Toolbar
1. 簡要
2. Menu的showAsAction屬性介紹
簡要
1. 如何將預設ActionBar關閉,改用Toolbar呢?
2. Toolbar Title如何置中呢?
3. 為Toolbar設定Menu

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

Android ActionBar與Toolbar

簡要

ActionBar是預設App的應用欄,但由於Android演化過程,ActionBar取決於使用哪個Android版本

相較之下最新的Toolbar可以在任何能使用支持庫的設備上使用

因此應使用Toolbar

最小能搭載在Android2.1,如果使用Material Design最小能搭載在Android5.0或更高的版本

Toolbar可以做到相同的功能且比Action Bar更靈活,能做的事情更多

接下來是本文所介紹的內容,以上內容參考Android官網

  1. 如何將預設ActionBar關閉,改用Toolbar呢?
  2. Toolbar Title如何置中呢?
  3. 為Toolbar設定Menu

1. 如何將預設ActionBar關閉,改用Toolbar呢?

首先先至app Bundle內,新增下列內容

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

繼承Theme.AppCompat.NoActionBar幫style設定風格顏色

app/src/main/res/values新建一個style.xml,然後加入以下程式碼

<style name="MainNoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="color">@color/purple_200</item>
<item name="colorAccent">@color/purple_500</item>
<item name="colorPrimary">@color/purple_700</item>
</style>

再來到AndroidManifest.xml將android:theme變更為下方

android:theme="@style/MainNoActionBar"

便可以在設定背景時,直接使用當前風格顏色

android:background="?attr/colorPrimary"

這樣設定好後,編譯並運行手機,會發現App ActionBar已經消失囉!

再來確保Activity是繼承AppCompatActivity

class MainActivity : AppCompatActivity() {
}

然後在Activity onCreate內新增以下程式碼,Toolbar就支援ActionBar囉!

setSupportActionBar(binding.mToolbar)

2. Toolbar Title如何置中呢?

如下預設的title文字只能靠左

Toolbar裡面的app:title="title"設定的時候文字會靠左,如下圖

左邊的Icon btn設定方式如下

supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_chevron_left_24)
supportActionBar?.setDisplayHomeAsUpEnabled(true)

監聽按鈕方式

mToolbar.setNavigationOnClickListener
或onOptionsItemSelected監聽item.itemId == android.R.id.home

設定後,title文字又被往右推一點

該如何把文字置中呢?

首先先把title刪除,改為下方程式碼

但title沒有設定時,會自動變成AndroidManifest.xml裡面android:label

可以在Activity onCreate裡面直接設定title = “",就可以清掉囉!

<androidx.appcompat.widget.Toolbar
android:id="@+id/mToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:navigationIcon="@drawable/ic_baseline_chevron_left_24"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="title"
android:textColor="@color/white"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>
上方程式碼效果圖

3. 為Toolbar設定Menu

經過上面設定後,Toolbar已經支援ActionBar了,所以設定的方式也跟ActionBar一樣

這裡舉例最簡單的Menu設定方式,其他的可以參考Android Menu showAsAction屬性

詳細Activity擁有應用欄Fragment擁有應用欄

參考這裡–>Android Activity、Fragment應用欄(AppBar)使用

以下為簡略的使用方法

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.activity_menu, menu)
    return true
}

監聽Menu點擊事件

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.searchMenu -> {
true
}
else -> super.onOptionsItemSelected(item)
}
}

以上內容參考Android 官網


相關文章

Android Menu showAsAction屬性Android Activity、Fragment應用欄(AppBar)使用
1. 簡要
2. Menu的showAsAction屬性介紹
簡要
1. Activity擁有應用欄
2. Fragment擁有應用欄
3. 動態修改Menu是否顯示
4. Menu點擊事件處理

訂閱Codeilin的旅程,若有最新消息會通知。

廣告

透過 WordPress.com 建置的網站.

向上 ↑