2021년 11월 16일 화요일

PlantUML 편집을 쉽게하기

PlantUML 편집을 쉽게하기

Android Studio 나 IntelliJ를 설치하여 plantUML plugin 을 설치하면 편집이 세상쉽다.

  1. 구글에서 android studio 를 검색해서 사이트로 가서 안드로이드 스튜디오를 설치한다.

enter image description hereenter image description here

  1. android studio 프로그램을 아무프로젝트나 만들든지 해서 시작하고, Preferences -> Plugins ->MarketPlace탭 에서 plant를 입력하면 [PlantUML integration] 이 나오는데 이를 인스톨하고 재시작한다.enter image description here
  2. 프로그램을 재시작한후에 File->new에 가보면 Plaunt UML 항목이 있다.enter image description here
  3. 생성된 파일을 열어서 이것저것 입력해보면 오른쪽에 미리보기가 나타나고 , 각 항목을 클릭하면 해당 라인에 선택이 된다. @,.@enter image description here
  4. 이런것도 만들수 있고…enter image description here
  5. 오른쪽 버튼을 누르면 저장하기도 된다.enter image description here
  6. 당연히 Git에 올리면 버젼관리도 된다.

2021년 11월 10일 수요일

Jacoco 테스트 커버리지

Jacoco 테스트 커버리지

JaCoCo 테스트 커버리지 평가툴

작업중인 프로젝트의 테스트가 제대로 작성되어 있는지를 검사하여 % 로 결과를 보여주는 툴이다.

프로젝트에 JaCoCo 플러그인 추가

grale.build.kts파일에 다음을 추가한다.

plugins {
  jacoco
}

jacoco {
  toolVersion = "0.8.5"
}

테스트결과 레포팅

테스트에 대한 커버리지 검사결과를 html,xml,csv형태로 만들어서 주기위해서 태스크를 추가한다.

task("jacocoTestReport", JacocoReport::class) {
    dependsOn("testDebugUnitTest")
    reports {
        xml.isEnabled = true
        csv.isEnabled = false
        html.isEnabled = false
    }
    sourceDirectories.setFrom("${projectDir}/src/main/java")
    classDirectories.setFrom("${buildDir}/tmp/kotlin-classes/debug")
    executionData.setFrom(files("${buildDir}/jacoco/testDebugUnitTest.exec"))
}

그밖의 https://qiita.com/beyondseeker/items/907e20a4da7d46c9de57
를 참고.

여러모듈일 경우 모듈별 결과를 merge태스트를 통해서 합할수도있다.
https://subroh0508.net/articles/jacoco-scripts-in-anroid-muitl-module-project-by-kotlin-dsl

2021년 11월 9일 화요일

Danger (CI 보조툴)

Danger (CI 보조툴)

Danger 란?

Github 등에 코드를 전송하여 CI툴을 연계시에 자주실수하는 기계적인 코드규칙에 대한 검사(LINT)를 수행할수 있도록 도와주는 프로그램이다.
주로 PR(Pull Request)할때에 발생하기 쉬운 문법적오류 등의 검사를 자동으로 해주는 툴이다.

사용법은 Danger가 설치된 로컬 또는 CI상에서 동작하며, danger 프로그램을 설치해야한다.

설치는 간단하다.
gem install danger
gem install danger_checkstyle_format ( 이것은 Danger plugin https://github.com/noboru-i/danger-checkstyle_format)

사용법은
danger --verbose --new-comment -dangerfile=./프로그램소스내에서 별도로 작성한 Danger파일
하면 작동한다.
아래는 예제이다.

  
# プルリクが編集中  
warn("このプルリクが編集中のようです。") if github.pr_title.include? "[WIP]"  
  
# プルリクがでかすぎる  
warn("プルリクの変更箇所が多すぎるので分割しましょう。") if git.lines_of_code > 500  
  
# マージコミットがある  
#has_merge_commit = git.commits.any? { |c| c.message =~ /^Merge branch '#{github.branch_for_base}'/ }  
#fail "マージコミットがあるのでリベースしましょう。" unless has_merge_commit  
  
# マイルストーンが設定されていない  
has_milestone = github.pr_json["milestone"] != nil  
warn("プルリクにマイルストーンが設定されていないようです。", sticky: false) unless has_milestone  
  
# 誰もアサインされていない  
has_assignee = github.pr_json["assignee"] != nil  
warn("誰もアサインされていないようです。", sticky: false) unless has_assignee

Bitrise를 연동한다면, Bitrise에서 script를 추가한후에

gem install danger
gem install danger-checkstyle_format
gem install danger-android_lint

danger --verbose --new-comment --dangerfile=./androidApp/LintDangerFile

을 추가한다.

Bitrise에서 Danger 를 github와 연계하려면 Env var에 DANGER_GITHUB_API_TOKEN 를 추가하여 값에 git token값을 입력하면된다.

danger-checkstyle_format 플러그인을 이용한다면,
1 . Bitrise의 workflow에 Gradle Task추가. ( 예로. :androidApp:check)
2 . Env var에 GRADLEW_PATH = ./gradlew 로 값추가.
3 . Bitrise의 workflow에 script추가하여 위에서 설명한 danger설치등…

checkstyle_format.base_path = Dir.pwd  
checkstyle_format.report "./androidApp/build/reports/ktlint-results.xml"
#(ktlint-results.xml이 필요하니까 Lint다음에 진행하도록한다)
# ktlint 시에 결과파일이 저기에 출력되도록 해둬야한다.
예:
val ktlintCheckANdroid by tasks.creating(JavaExec::class) {
...  
args = listOf(  
    "--reporter=plain",  
    "--reporter=checkstyle,output=${buildDir}/reports/ktlint-results.xml",  
    "src/**/*.kt"  
)...
}

설정이 제대로 되었다면

  1. 사용자가 소스 commit->push->pr작성
  2. Bitrise가 가동
  3. Bitrise의 workflow 가 진행되면서 ktlint->danger 실행됨
  4. github pull request 를 보면 ktlint에서 오류를 잡아낸 것을 danger가 분석해서 github 화면에 라인별로 보여줌.

2021년 10월 1일 금요일

cocos2dx v4.0 xcode 샘플빌드

cocos2dx v4.0 xcode 샘플빌드

cocos2dx 4.0 을 다운받았으면,
프로젝트를 생성하고 프로젝트폴더밑에 iosProject 라는 대충 폴더 만들고 아래를 실행해서 xocde용 프로젝트 파일을 만들어야한다.
cmake … -GXcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
xxx. xcodeproj 을 실행하고 빌드해보면 안된다.

유저폴더에 libiconv.dylib 얘랑 다른 뭐랑 없단다.
컴퓨터에 있는 파일이다. 찾자.
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libiconv.dylib
에있다.

xcode의 project->build settings -> linking -> other link flag 를 보면 debug 항목이 있는데 클릭하면 맨밑에 에러난 두개(?
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libz.dylib
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libiconv.dylib) 파일이 보인다.
이 경로를 위의 경로로 바꿔치기한다.

2021년 5월 7일 금요일

Kotlin Koin

Kotlin Koin

Koin

Di (Dependense Injection) するためにはDagger2, HiltがあるがKotlinらしくなくて使い方がやや口説くなることがあったのでIntelliJ社はKotlin専用のDIライブラリのKoinを作った。
KoinはKotlinらしく使いやすい、簡単なのでDIなんかを初めて触った素人も即座で使える。

https://insert-koin.io/

早速やって見よう。

  1. build.gradle(Project: xxx) に追加
// Add Maven Central to your repositories if needed
repositories {
  mavenCentral()
}
dependencies {
  // Koin for Android
  compile "org.koin:koin-android:$koin_version"  
 // Testing
 testCompile "org.koin:koin-test:$koin_version"
}
  1. build.gradle(Module: xxx) に追加 (必要なものだけに加減する)
  
def koin_version = "2.1.5"  
  
// For Android  
// Koin for Android  
implementation "org.koin:koin-android:$koin_version"  
// Koin Android Scope features  
implementation "org.koin:koin-android-scope:$koin_version"  
// Koin Android ViewModel features  
implementation "org.koin:koin-android-viewmodel:$koin_version"  
// Koin Android Experimental features  
implementation "org.koin:koin-android-ext:$koin_version"    
testImplementation "org.koin:koin-test:$koin_version"
  
  
// For AndroidX  
// Koin AndroidX Scope features  
implementation "org.koin:koin-androidx-scope:$koin_version"  
// Koin AndroidX ViewModel features  
implementation "org.koin:koin-androidx-viewmodel:$koin_version"  
// Koin AndroidX Fragment features  
implementation "org.koin:koin-androidx-fragment:$koin_version"  
// Koin AndroidX Experimental features  
implementation "org.koin:koin-androidx-ext:$koin_version"
  1. 必要なモジュールを先に作る。
    Entity->Domain(Usecase, Repository) ->Module順。
    今回の例はちょっと違和感があるな。。
//-----------------------//
// package name/domain/entity/Article.kt
//-----------------------//
data class Article(  
    var say : String = "",  
)

//-----------------------//
// package name/domain/usecase/MyUseCase.kt
//-----------------------//
class MyUseCase (  
   private val localRepository: LocalRepository  
)  {  
    fun sayHelloMessageShowUseCase() : String {  
        return localRepository.getHelloMsg()  
    }  
}

//-----------------------//
// package name/domain/repository/LocalRepository.kt
//-----------------------//  
class LocalRepository(  
    private val databaseModule: DatabaseModule  
)  
{  
    fun getHelloMsg() = databaseModule.getDashboard().say  
}

//-----------------------//
// package name/di/DatabaseModule/DatabaseModule.kt
//-----------------------//  
class DatabaseModule(  
    val article: Article  
) {  
    fun getDashboard() : Article {  
        article.say = "Say Hello Koin"  
  return article  
  }  
}
  1. Application ファイルを作ってstartKoinをする。
    本当にシンプルすぎる。
class App : Application() {  
    override fun onCreate() {  
        super.onCreate()  
        startKoin {  
  androidContext(this@App)  
            modules(module {  
  factory { Article() }  
  single { DatabaseModule(get()) }  
  single { LocalRepository(get()) }  
  factory { DashboardUsecase(get()) }    
  viewModel { DashboardViewModel() }
 })  
        }  
  }  
}
  1. ViewModelで使う。KoinでViewModel用のモジュールを使うにはviewModelというキーワードを使わなきゃならないので注意。(viewModel { someClass() } )

普通はのInjectしたいとことにKoinComponentを継承すればできるはず。

//-----------------------//
// package name/ui/dashboard/DashboardViewModel.kt
//-----------------------//  
class DashboardViewModel : ViewModel(), KoinComponent {    
     private val usecase: sayHelloMessageShowUseCase by inject()    
    private val _text = MutableLiveData<String>().apply {  
  value = "This is dash Fragment"  
  }  
  val text: LiveData<String> = _text    
  fun change() {  
        _text.postValue(usecase.changeText())  
    }  
}

//-----------------------//
// package name/ui/dashboard/DashboardFragment.kt
//-----------------------//    
class DashboardFragment : Fragment(), KoinComponent {  
 val dashboardViewModel: DashboardViewModel by viewModel()
       
lateinit var textView: TextView  
override fun onCreateView(  
    inflater: LayoutInflater,  
    container: ViewGroup?,  
    savedInstanceState: Bundle?  
): View? {  
    val root = inflater.inflate(R.layout.fragment_dashboard, container, false)  
    textView = root.findViewById(R.id.text_dashboard)  
    dashboardViewModel.text.observe(viewLifecycleOwner, Observer {  
  textView.text = it  
 })  
    textView.setOnClickListener {  
  dashboardViewModel.change()  
    }  
  return root  
}
  1. TestCodeはこうやる
class SampleKoinTest : KoinTest {  
  
    private val usecase: DashboardUsecase by inject<DashboardUsecase>()  
  
    @get:Rule  
  val koinTestRule = KoinTestRule.create {  
  modules(AppModules().modules)  
    }  
  
  
  @Test  
  fun sampleTest() {  
        assert("Say Hello Koin" == usecase.changeText())  
    }  
}
  1. 終わり。アプリを走ってみるとDIがキチンと動く。

これは。。これは。。素晴らしいすぎる。
どんどんIntellJ社に支配されている感じがする。怖っ。。

KoinはLight weight DI ライブラリーなのでDaggerより多少の機能が足りないかもしれないがこれて十分だ。

2021년 5월 6일 목요일

Dagger2 ( Java -> Kotlin )

Dagger2 ( Java -> Kotlin )

Dagger2 ( Java -> Kotlin )

Dagger から Hilt, Koinに 引越しする前に覚えたことを忘れないうちに書いておきたいと思ってここに書き留める。

参考:DIというのは。
DIは外部から機能を注入することでなるべくインスタンスを生成しないようにするのが基本概念だ。
DIを使わないとインタフェースを使って同じく実現できるがそんなことしようとすると作るファイル数が増えるのでめんどくさくなるので普通はDIライブラリーを使う。
プログラミングにてDIの仕組みは外部に自分が定義したモジュール単位のクラスを使いたいインスタンス変数にタイプを合わせて注入して使うことだ。
作ったモジュールがインスタンス変数に注入されるのはDIライブラリーがやってくれるので指定だけやればDIが実現できる。
Android には Dagger, Hilt, Koin などが普通。

Daggerから DI をするためには以下の概念を理解する必要がある。

  1. Component : こいつは病院の役割だ。Inject(注射を受けるやつ)に必要な色んな薬を持っている。格製薬会社が作ったModule(薬)を必要な時に要請しているところに注射をする。
  2. Subcomponent : 大きい病院の役割がデカくなった場合には各分野ことに切り分けて専門にする小さい病院みたいに小分けして親子関係ようにするのが可能だ。
  3. Module & Provides : ModuleはComponentに薬を提供する製薬会社だ。Component(病院)はModule(製薬会社)と契約して指定された薬をProvides(提供)する。ModuleとProvidesは定義するだけで実際に注射するやつはComponentだ。
  4. Scope : DaggerではApplication単位を制御するために@Singletonを指定してアプリケーション全盤の範囲にて活動をしようとしている。なので使っているActivityとかFragmentとかに限られたInjectが必要な場合には余計なメモリを使いたくないから別途でScopeを指定する必要がある。(Dagger2@PerFragment,@PerActivityなどを提供している。無論、自分のScopeも作れる)
  5. Inject : Inject (注射を要請)するものにComponentが適当なModule(製薬会社)のProvides(薬)を処方して注射を受ける。

Basic

Dagger はAndroid専用のライブラリではない。なのでbuild.grade(Module:app) に三つのライブラリを追加する。

plugins {  
  ...
  id 'kotlin-kapt'  
}

dependencies {
	...
	// dagger2  
	def DAGGER_VERSION = '2.26' 
	implementation "com.google.dagger:dagger:$DAGGER_VERSION"  
	kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"  
	implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"  
	implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"  
	kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
   ...
}

ライブラリ名の通りにDaggerがAndroidを支援する(DaggerApplication, AndroidInjectorなとのインターフェース)ために必要だ。
Daggerを使う目的はClass間に依存関係を減らすためなのでTopLevel(アプリケーション単位)でDagger関連の宣言が必要だ。
宣言は三つぐらいがありそれぞれのパタンーで作れる。

  1. すでにアプリケーションが作られているのでここからDaggerを導入しようとしたら既存のソースに影響がされたくない時には既存のコードに必要なDagger宣言をする。
// MyApplicationはすでにあった場合
public class MyApplication extends Application {  

    public static MyFirstComponent myFirstComponent;  
  
    @Override  
    public void onCreate() {  
        super.onCreate();  
      // 既存のコードにこれを追加する
        myFirstComponent = DaggerMyFirstComponent  
                .builder()  
                .build();  
    }  
}

//-----------------------//
// package name/mydi/components/MyFirstComponent.java
//-----------------------//
// Singletone スコープはアプリケーションの全範囲
// このコンポーネントはMyFirstModule.classを持つ
// Daggerが自動にDaggerMyFirstModuleというクラスを作る
@Singleton  
@Component(modules = {MyFirstModule.class})  
public interface MyFirstComponent {  
  FirstViewModel firstViewModel();  
  SecondViewModel secondViewModel();    
}

//-----------------------//
// package name/mydi/modules/MyFirstComponent.java
//-----------------------//
@Singleton  
@Module  
public class MyFirstModule {  
  
    @Singleton  
    @Provides  public FirstViewModel firstViewModel() {  
        return new FirstViewModel();  
    }  
  
    @Singleton  
    @Provides  public SecondViewModel secondViewModel() {  
        return new SecondViewModel();  
    }  
  
}

//-----------------------//
// package name/viewmodel/FirstViewModel.java
//-----------------------//
public class FirstViewModel {  
  @Inject  
  public FirstViewModel() {}  
    public String getSay() {  
        return "Say First Module";  
    }  
}
//-----------------------//
// package name/viewmodel/SecondViewModel.java
//-----------------------//
public class SecondViewModel {  
  @Inject  
  public SecondViewModel() {}  
    public String getSay() {  
        return "Say Second Module";  
    }  
}

// 
  
public class MainActivity extends AppCompatActivity {  
    
 @Inject
  public FirstViewModel firstViewModel;  
   
 @Inject
  public SecondViewModel secondViewModel;
    
	@Override  
	protected void onCreate(Bundle savedInstanceState) {  
	    super.onCreate(savedInstanceState);
	      
		firstViewModel = MyApplication.component.firstViewModel();  
		secondViewModel = MyApplication.component.secondViewModel();
	}
}

一旦、FirstViewModelとSecondViewModelがMainActiviy内でnewを使わなくてもインスタンスを生成できたのは良いところだ。
しかし、これは依存関係を減らしただけだ。新しいものが追加されたら直接に修正しなければいけないのでこちらはあんまりに使わないかもしれない。

  1. HasAndroidInjectorインターフェースが出来てそれを使う。
//-----------------------//
// App.kt
//-----------------------//
class App : Application(), HasAndroidInjector {  //HasAndroidInjectorを継承する
  
    @Inject lateinit var androidInjector: DispatchingAndroidInjector<Any>  
  
    override fun androidInjector(): AndroidInjector<Any> {  
        DaggerAppComponent.factory().create(this).inject(this)  
        return androidInjector  
  }  
}
//-----------------------//
// AppComponent.kt
//-----------------------//
@Singleton  
@Component(  
    modules = [  
        AndroidInjectionModule::class,  //AndroidSupportInjectionModuleからAndroidInjectionModuleに変更
        ActivityBindingModule::class,  
        AppModule::class  
  ]  
)
//-----------------------//
// MainActivity.kt
//-----------------------//
  
class MainActivity : AppCompatActivity(), HasAndroidInjector {  
  @Inject  
  lateinit var androidInjector: DispatchingAndroidInjector<Any>  
  
  @Inject  
  lateinit var prefs: SharedPreferenceStorage    
    override fun onCreate(savedInstanceState: Bundle?) {    
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_main)    
        
        AndroidInjection.inject(this)  
    }  
  
    override fun androidInjector(): AndroidInjector<Any> {  
        return androidInjector  
    }  
}
  1. 最初からアプリケーションを作るならDaggerが提供するクラスを継承して簡単(?)に作れる。
//-----------------------//
// App.kt
//-----------------------//
// DaggerApplicationを継承してAppComponenのfactoryを実行する
class App : DaggerApplication() {  
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {  
        return DaggerAppComponent.factory()  
            .create(this)  
    }  
}

//-----------------------//
// your package/di/AppComponentt.kt
//-----------------------//
// このファイルはDaggerがDaggerAppComponentに実装して提供する
@Singleton  
@Component(  
    //アプリに使うModuleを定義する
    modules = [  
        AndroidInjectionModule::class,  
        AppModule::class,  
        MainActivityBuilder::class,  
        RepositoryModule::class  
  ]  
)  
interface AppComponent : AndroidInjector<App> {  
    @Component.Factory  
    interface Factory {  
        fun create(@BindsInstance app: App): AppComponent  
    }  
}

//-----------------------//
// your package/di/AppModule.kt
//-----------------------//
// Global Application ContextためのModule
@Module  
abstract class AppModule {  
    @Binds  
   abstract fun provideContext(application: App): Context    
}
@Singleton  

//-----------------------//
// your package/di/ui/MainActivityBuilder.kt
//-----------------------//
// ContributesAndroidInjectorにより自動にMainActivityModuleが連携される
@Module  
abstract class MainActivityBuilder {  
  
    @ContributesAndroidInjector(modules = [MainActivityModule::class])  
    abstract fun bindMainActivity(): MainActivity  
}

//-----------------------//
// your package/di/ui/MainActivityModule.kt
//-----------------------//
@Module  
interface MainActivityModule {  
    @Binds  
 @IntoMap @ViewModelKey(MainViewModelImpl::class)  
    fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel  
  
    @ContributesAndroidInjector  
  fun bindMainFragment(): MainFragment  
}


//-----------------------//
// your package/di/ui/MainActivityModule.kt
//-----------------------//
@Module  
interface MainActivityModule {  
    @Binds  
 @IntoMap @ViewModelKey(MainViewModelImpl::class)  
    fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel  
  
    @ContributesAndroidInjector  
  fun bindMainFragment(): MainFragment  
}

//-----------------------//
// your package/ui/MainFragment.kt
//-----------------------//
class MainFragment : DaggerFragment() {  
    @Inject  
  lateinit var viewModelFactory: ViewModelFactory  
    private val viewModel: MainViewModel by lazy {  
  ViewModelProvider(this, viewModelFactory).get(MainViewModelImpl::class.java)  
    }  
  
  override fun onCreateView(  
        inflater: LayoutInflater, container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ): View? {  
        viewModel.getRepo()  
  
        viewModel.repo.observe(this, Observer {  
  Toast.makeText(  
                context,  
                "id = ${it.id}, title = ${it.title}, content = ${it.content}",  
                Toast.LENGTH_SHORT  
  ).show()  
        })  
        return inflater.inflate(R.layout.fragment_main, container, false)  
    }  
  
  
}


//-----------------------//
// your package/ui/MainViewModel.kt
//-----------------------//
  
interface MainViewModel {  
    val repo: LiveData<Repo>  
    fun getRepo()  
}  
  
class MainViewModelImpl @Inject constructor(private val repository: MainRepository) : ViewModel(),  
    MainViewModel {  
    override val repo = MutableLiveData<Repo>()  
    override fun getRepo() {  
        val data = repository.getRepo()  
        repo.postValue(data)  
    }  
}
  
//-----------------------//
// your package/di/RepositoryModule.kt
//-----------------------//
@Module  
internal object RepositoryModule {  
    @Singleton  
 @Provides  fun provideMainRepository(): MainRepository =  
        MainRepositoryImpl()  
}
  
//-----------------------//
// your package/MainRepository.kt
//-----------------------//
interface MainRepository {  
    fun getRepo(): Repo  
}  
  
class MainRepositoryImpl : MainRepository {  
    override fun getRepo(): Repo {  
        return Repo(id = 1, title = "タイトル", content = "内容")  
    }  
}

data class Repo(  
    val id: Int,  
    val title: String,  
    val content: String  
)


//-----------------------//
// your package/di/viewmodel/ViewModelKey.kt
//-----------------------//
@MustBeDocumented  
@Target(  
        AnnotationTarget.FUNCTION,  
        AnnotationTarget.PROPERTY_GETTER,  
        AnnotationTarget.PROPERTY_SETTER  
)  
@Retention(AnnotationRetention.RUNTIME)  
@MapKey internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

//-----------------------//
// your package/di/viewmodel/ViewModelFactory.kt
//-----------------------//
class ViewModelFactory @Inject constructor(  
    private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>  
) : ViewModelProvider.Factory {  
  
    override fun <T : ViewModel> create(modelClass: Class<T>): T {  
        var creator: Provider<out ViewModel>? = creators[modelClass]  
        if (creator == null) {  
            for ((key, value) in creators) {  
                if (modelClass.isAssignableFrom(key)) {  
                    creator = value  
                    break  
  }  
            }  
        }  
        if (creator == null) {  
            throw IllegalArgumentException("unknown model class " + modelClass)  
        }  
        try {  
            @Suppress("UNCHECKED_CAST")  
            return creator.get() as T  
  } catch (e: Exception) {  
            throw RuntimeException(e)  
        }  
    }  
}

HiltやKoin使うともっと簡単に設定ができる!
HiltやKoin使おう!

Android 개발팁메모장

  • 라이프사이클

    1. 외워서 그릴줄 알아야 한다.
      Activity/Fragment 생명주기
      https://techbooster.org/wp-content/uploads/2014/12/lifecycle.png
      https://github.com/xxv/android-lifecycle
    2. onAttach(Fragment Only)
      Fragment 에만 있는 싸이클로서 Fragment 가 Activity 에 붙을때 실행된다.
    3. onActivityCreated(Fragment Only)
    4. onCreate
      액티비티를 최초에 실행할때 호출한다. 리소스 , layout 등의 초기화 작업을 한다. 또한 액티비티가 살아있는 동안에 유지해야 할 변수나 기능들을 초기화하고 보관해놓는다.
    5. onCreateView(Fragment Only)
      Fragment 에만 있는 싸이클로서 Fragment 의 경우는 여기서 layout 처리를 한다. Fragment 에서는 onCreate 에서 view 나 ui 관련 작업을 할수 없다.따라서 이곳에서 view 의 초기화, ui 관련 사항들을 지정할수있다. 뷰의 요소등에 대한 처리등 시간이 걸리는 로직은 onViewCreated 에서 하자.
    6. onViewCreated(Fragment Only)
      Fragment 에 view 가 초기화 되었다는 것을 알려준다. 인수로 view 를 보내주기 때문에 뷰의 각 요소에 대한 초기화 등을 할 수 있다.
    7. onActivityCreated(Fragment Only)
      Activity 가 화면에 보이는 상태이다 UI 조작이 가능한다. 단 Fragment 가 처음 실행될때만 실행된다.이때는 Activity 와 Fragment 가 연결된 상태이므로 두 객체간 상호작용이 가능하다.
    8. onStart
      UI가 화면에 보이게 하는단계이다.따라서 여기서 긴 작업을 하면 그만큼 UI 화면에 보이는것에 지장을 주게 되기 때문에 되도록 긴작업은 하지않는게 좋다.
    9. onRestart
      액티비티가 onStop 상태로 되어 배터리소모를 위해 해제했던 리소스나 리스너,리시버 등을 여기서 다시 등록, 점검하도록 해야한다. 준비가 되었다면 화면을 다시 복원시킬 onStart 가 실행된다.
    10. onResume
      액티비티가 onPause 상태에서 다시 화면이 활성화 되고, 사용자입력을 받을수 있는 상태가 되기 때문에 onPause 에서 해제해두었던 서비스나 기능을 다시 활성화 한다.
    11. onPause
      다른 액티비티나 앱에 의해 화면이 일부 가려진 상태이다. 액티비티가 사용자의 입력을 멈추고 임시적인 활동정지 상태로 들어간다. 이 상태는 화면갱신,사용자입력등을 받을수없는 상태이기 때문에 불필요하게 실행되고 있는 센서,카메라,GPS, 브로드캐스트 등을 정지하여 배터리가 소모되지 않도록하는게 좋다. 단, 데이터를 저장소에 저장하거나, 데이터베이스등을 다루는 작업을 하지않는다. 왜? onPause 는 긴작업을 할만한 시간을 가지고 있지않다.onStop 에서 처리하는게 좋다.
    12. onStop
      Activity 가 완전히 백그라운드로 이동되어 화면에 UI가 완전히 보이지않을때 호출된다. 따라서 이 단계에서는 현재 액티비티에서 만들었던 모든 메모리를 해제하고, 사용자정보등을 디비나 서버에 저장하는게 좋다. 이 단계에서는 아직 UI 관련 정보와 Activity 관련 정보가 시스템 메모리에 남아있어서 onStart 에서 빠르게 화면을 다시 복구 할수 있도록 해준다.
    13. onDetachView(Fragment Only)
      액티비티가 종료될때 호출된다. 액티비티의 종료는 사용자에 의해서 일어날수도 있지만 시스템에 의해서 강제로 발생할수 있다 따라서 여기서 긴시간이 걸리는 작업을 하게되면 불안정하게 종료될 가능성이 있기 때문에, 그런것들은 onStop 에서 처리해두는게 좋다.
      단 , 프로그램에서 finish() 를 하게되면 onResume->onDestory 형태로 바로 로딩되기때문에 이떄는 onDestroy 에서 처리해주어야줄 필요가 있다.
    14. onDetach(Fragment Only)
      액티비티에서 fragment 가 완전히 연결이 끊길때 실행된다. 프래그먼트에서 사용됬던 각종 메모리를 해제한다.
    15. onDestroy
      프래그먼트가 더이상 메모리에 있을 필요가없을때 실행되기 때문에 프래그먼트에서 사용했던 메모리를 해제한다.
  • Fragment

    1. Fragment를oncreate 하거나 attach/reattach 하면 onActivityCreated가 불려지는데, 이곳에서 live 데이터의 Observer 를 등록하면 무수히 많은 observer 인스턴스가 생성된다. 따라서 메모리소비, 복수의 통지수신을 받는경우가 생길수도 있다. Observer 를 등록할때, getViewLifecycleOwner() 또는 getViewLifecycleOwnerLiveData()를 이용하자.
      viewModel.getMainTab().observe(getViewLifecycleOwner(), new Observer<Integer>() {
          @Override
          public void onChanged(@Nullable Integer integer) {
              //Do something
          }
      });
      
  • RecycleView

    1. RecycleView
      1. 리사이클뷰의 현재상태를 유지하고 싶을때는 onPause 에서 .
			protected Parcelable rcListState;
		    @Override
		    public void onPause() {
		        super.onPause();
		        rcListState= rcList.getLayoutManager().onSaveInstanceState();
		    }
		    @Override
		    public void onResume() {
		        super.onResume();
				if (rcListState!= null) {
		                rcList.getLayoutManager().onRestoreInstanceState(rcListState);
		                rcListState= null;
		        }
		    }
    2. 리사이클뷰가 스크롤이 끝났을때 콜백액션
    Cannot call this method while RecyclerView is computing a layout or scrolling
처럼에러가 나올때는 스크롤이 끝난 시점에 추가로드 등을 하기위해서 리사이클뷰의 post 에서 실행하면된다.
		~~~java
		@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
		  super.onScrolled(recyclerView, dx, dy);

		  if() {
		     recyclerView.post(callbacks); 또는
		     recyclerView.post(() -> 실행할메소드());또는
		     recyclerView.post(new Runnable() {
		                @Override
		                public void run() {
		                   실행할메소드()
		                }
		      });
		  }
		}
		~~~
  1. ViewPager
    1. Android ViewPager.OnPageChangeListener 의 이벤트 전달 순서.
      https://indienote.tistory.com/74?category=644011
      참고하자.
  2. CustomView
    1. 커스텀뷰에 에 xml 파라메터 만들기.
      <?xml version="1.0" encoding="utf-8"?>
       <resources>
       <declare-styleable name="OwnCustomView">
       	<attr name="fillColor" format="color"/>
       </declare-styleable>
       </resources>
       레이아웃에서는
       <com.customview.OwnCustomView
       	android:layout_width="match_parent"
       	android:layout_height="wrap_content"
       	app:fillColor="@android:color/holo_blue_dark"/>
      
      커스텀뷰에서는
      int fillColor;
       TypedArray ta =
       context.getTheme().obtainStyledAttributes(attributeSet,
       R.styleable.OwnCustomView, 0, 0);
      
       try {
       fillColor =
       ta.getColor(R.styleable.OwnCustomView_ocv_fillColor,
       DEFAULT_FILL_COLOR);
      
       } finally {
       ta.recycle();
       }
       
       OwnCustomView customView = new OwnCustomView(this);
       customView.setFillColor(BRIGHT_GREEN);
      
  • 빌드
    1. 빌드상수로 디버그빌드와 릴리스빌드때 서로 다른 서버URL값 설정하기.
      프로그램에서 서버 URL 등을 디버그나 테스트,개발시에 다르게 설정할수 있는데 Gradle 에서 설정한 값들은 java 에서는 “BuildConfig.속성” 처럼 상수로 접근할수 있다.

      buildTypes{
          release {
              buildConfigField "String", "SERVER_URL", '"https://www.release.com"'
          }
          debug {
              buildConfigField "String", "SERVER_URL", '"https://www.test.com"'
          }
      
    2. ProGuard
      프로가드는 minifyEnabled 를 true 로 줌으로서 활성화할수있다. 이런저런 라이브러리를 사용하여 난독화하면 단일dex 파일이 너무 커지거나 라이브러리의 난독화과정에서 빌드에러가 나오는경우도 있는데 그럴경우

      proguard-rules.pro파일에
      -dontwarn okio.**
      -dontwarn retrofit2.**
      

      처럼 추가해주면된다.
      디버그 상태에서는 별도의 프로가드 설정을 주거나 [proguardFile ‘proguard-debug.pro’] minifyEnabled 를 false 로 지정하여 원활히 디버깅이 되도록 할수있다.
      또한 각각의 라이브러리는 별도의 프로가드 파일을 지정하여 주면 좋다.

      proguardFiles 'proguard-rules-retrofit.pro'
      proguardFiles 'proguard-rules-rx.pro'
      
    3. shrinkResources
      불필요한 리소스를 제거해준다. true 로 해준다.

    4. gradlewrapper
      gradlew 명령어로 gradlew clean build assemble 하면 gradle 시에 에러 정보가 보인다.

  • Android Jetpack lifecycle 소개 및 구현 방법 (LiveData, ViewModel 연계)
    1. 미친 안드로이드가 아예 lifecycle 이라는 라이브러리를 만들어서 onResume (또는 onStart,onRestart)에서 register 를 onPause(또는 onStop) 에서 unregister 해주던 것들(리스너등) 을 lifecycle 에 옵서버형태로 옮겨서 자동으로 Activity 라이프사이클에 맞게 자동으로 on/off 될수 있도록 하는걸 만들언다.
      • Lifecycle : lifecycle 객체자체, 즉 subscriber 다.
      • LifecycleOwner : lifecycle 을 포함한 말그대로 생명유지를 하는 주체, 즉 Activiy 다.
      • LifecycleObserver : Owner 의 상태를 감지해서 이벤트를 발생시키는 옵서버.
        이런 라이프사이클을 이용해서 자동으로 라이프사이클에 맞춰 연동되도록 되어 있는 클래스들을 lifecycle-aware components 라고 한다.
    2. LiveData 라는 것은 LifecycleOnwer 의 생명주기에 맞게 연결된 UI 객체에 값변경을 통지하는 역활을 대신 편리하게 해준다. 예전에는 액티비티나 프래그먼트 생명주기에 맞게 UI 에 뭔가를 다시 출력하거나 하는게 귀찮았는데, LiveData 로 연결만해주면 Lifecycle 즉 , 액비티티나 프래그먼트의 생명주기에맞게 화면에 보일때만 변경표시를 해주고 owner 가 삭제되었을때는 연결된 데이터도 삭제해서 GC 대상이 될수 있도록 하여 메모리누수를 덜 일으키도록 도와준다.
    3. ViewModel 안드로이드에서 ViewModel 클래스는 액비티니,프래그먼트에 포함된 UI 조작(로직)을 따로 분리 할수 있도록 도와주는 클래스로서 ViewModel 에 연결된 UI 는 액티비티나 프래그먼트의 생명주기와 관계없이 언제든지 통지를 받아 변경될수있는 상태가 되도록 한다.즉 액티비티가 재실행되거나, 멈춤,복귀 되는 등(화면회전, SMS 메시지확인,전화받기등)의 과정과 관계없이 주욱 데이터를 유지시키며 이를 가능하게 하는게 LiveData 이다.
      지저분하게 Activity, Fragment 에서 + , - 이건거 하지 말자.
      (아차차…참고1 .뷰모델이 자신이 가진 메모리를 내밷고 생명을 다하는 경우는 Acitivity의 경우 finish 되었을때나 Fragment의 경우 detached 가 삭제될경우다)
      (아차차…참고2 .뷰모델은 스스로 장수하는 반면, Acitivity나 Fragment는 경우에따라 죽었다 살아나기를 반복하면서 이름을 바꾸기 때문에 직접 이들의 Context 를 직접 참조하게 되면 에러를 내뿜게 된다. ApplicationContext 하거나 AndroidViewModel 을 확장받아서한다. )
    4. LiveData 의 Transformation , LiveData의 값이 변경되어 대상객체에 통지하기 전에 항상 그값에 뭔가를 추가처리하고자 할때 사용한다. [Transformation.map(소스, 함수)=스트링 리턴값] 과 [Transformation.switchMap(소스, 함수)=라이브데이터 리턴값] 과 [mediatorLiveData가 있다.
      • Transformations.map : 라이브 데이터가 갱신되면 지정된 함수에서 처리된 결과값이 strign 으로 반환된다.
  1. TEST

    1. Androidx 테스트 지원 라이브러리 https://developer.android.com/training/testing/set-up-project
    2. 빌드상수로 디버그빌드와 릴리스빌드때 서로 다른 서버URL값 설정하기.
      프로그램에서 서버 URL 등을 디버그나 테스트,개발시에 다르게 설정할수 있는데 Gradle 에서 설정한 값들은 java 에서는 “BuildConfig.속성” 처럼 상수로 접근할수 있다.
      buildTypes{
          release {
              buildConfigField "String", "SERVER_URL", '"https://www.release.com"'
          }
          debug {
              buildConfigField "String", "SERVER_URL", '"https://www.test.com"'
          }
      
    3. 신규 프로젝트 만들고 Test 봤더니 @RunWith(AndroidJUnit4.class) 에 빨간줄갈때
      구글은 정말…어쩌라구…
      dependencies {  
      	androidTestImplementation 'androidx.test:rules:1.2.0' 이건rule 위해.
          androidTestImplementation 'androidx.test.ext:junit:1.1.2-alpha02' 추가
          androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' Intent,hasComponent 사용		    
      }
      
    4. 런쳐액티비티 테스트
      public class TCS1_010 {  
        
          @Rule  
        public final ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);  
        
          @Test  
        public void startApp() {  
               Intent intent = new Intent();  
              // intent.putExtra("your_key", "your_value");
              mActivityTestRule.launchActivity(intent);  	  		  
          }  
        
      }
      
    5. 액티비티와 프래그먼트의 테스트는 여길참조 The Basics of Android Espresso Testing: Activities & Fragments
  2. SQLite

    1. SQLite의 느린속도때문에 쿼리스트링 만들때는 stringbuffer 를 사용하자.
    2. compileStatement 를 사용해서 안전하고 빠르게 삽입하자.
      https://medium.com/androidstarters/mastering-android-studio-templates-ed8fdd98cb78
      https://medium.com/gits-apps-insight/tutorial-create-your-own-template-for-android-studio-1aaa9b4cb18
      AndroidStudio\plugins\android\lib\templates\밑에 있는 파일대로 만들면된다.
      }
      
  3. ETC

    1. AndroidStudio 에서 액티비티,플래그먼트,리사이클뷰세트 등을 만들때, 템플릿을 이용해서 기준포맷된 화일을 빠르게 생성하도록할수 있다. 협업작업때 기존화일을 카피해서 안쓸꺼만 지우고 하다보면 시간도 가고 실수도 하는데 차라리 템플릿으로 만들어 두면 좋다.
      또, 새로운 프로젝트를 템플릿으로 만들어 두면 빠르게 새로운 프로젝트를 시작할수 있다.
      일단 , 참고 했던곳,
      https://medium.com/androidstarters/mastering-android-studio-templates-ed8fdd98cb78
      https://medium.com/gits-apps-insight/tutorial-create-your-own-template-for-android-studio-1aaa9b4cb18
      AndroidStudio\plugins\android\lib\templates\밑에 있는 파일대로 만들면된다.
      }
      

2021년 4월 26일 월요일

JetPack-Navigation-Component

JetPack-Navigation-Component

Android Navigation Component

Jetpack의 Navigation 기능제공으로 안드로이드에서도 XCode처럼 화면간 이동관계를 그래프형식으로 보면서 작성할수 있게 되었다.

Safe-Args

화면(Activity 또는 Fragment)사이에 데이터를 전달하기 위해서는 Bundle에 담아서 하곤했는데, Safe-Args를 이용하면 데이터를 전달할때 서로다른 타입으로 처리하려고 하는에러를 미리 방지할수 있고 좀더 간단하게 처리할수 있도록 도와준다.

build.gradle(Project: xxx)

의존 class path 를 추가한다.

buildscript {  
  ...
  dependencies {  
  ...
  classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"  
  
  }  
}

build.gradle(Module: xxx)

Safe-Args는 Gradle에서 Plugin 형태로 동작한다. 이 플러그인은 나중에 safearg로 지정된 데이터타입을 기반으로 자동으로 해당 코드들을 생성해준다.

apply plugin: "androidx.navigation.safeargs.kotlin" // kotlin only
// apply plugin: "androidx.navigation.safeargs" java and kotlin ( will be generated java source code)

또는 

plugins {  
   ...
  id 'androidx.navigation.safeargs.kotlin'  // kotlin only
  // id "androidx.navigation.safeargs" java and kotlin ( will be generated java source code)
}


// navigation 을 사용하기 위해추가    
dependencies {
   ...
  def nav_version = "2.3.5"  
  
  // Kotlin  
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"  
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"  
}

Available data type of Safe Args

기본타입(Integer, Float, Long, Boolean) 부터 String, Parceable, Serializable, Enum, resource reference 등까지 가능하다.

Layout Setting

Navigation 은 특정 Component(주로 Fragment) 에 NavHostFragment 컨테이너를 지정하여 지정된 fragment 가 Host 역활을 하도록 하게 한다.
만일 Fragment 을 이동하는거라면 같은 Activity 내에서 호스트영역에 바꿔가며 보여주는 것이 가능하고, Activity 라면 새로운 화면으로 이동하는것이 가능하다.

  1. 먼저 layout design 화면에서 navigation 그래프를 표시하기 위해서 navigation resource를 만들자.
// your_nav_graph.xml
<!-- res 폴더에서 오른쪽버튼 - New - Android Resource File 을 선택한다음, 대화상자에서 Resource type: Navagation 을 선택하여 navigation graph 파일을 생성한다. -->   
<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/your_nav_graph"  
  app:startDestination="@id/yourFistFragment"> 
</navigation>
  1. 기본 액티비티 레이아웃파일에 navigation container 를 추가하여 아래처럼 만들자. 편의를 위해서 하단 탭버튼을 자동으로 생성해주는 bottomnavigation 도 추가했다.(탭 아이콘을 추가하면 자동으로 네비게이션의 각 화면으로 이동해준다)
// activity_main.xml
<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=".MainActivity"  
  >  
    
<!-- 아래의 fragment 를 직접 입력해도 되고, Palete - Containers - NavHostFragment 를 activity화면에 드래그 하면 자동으로 아래의 태그가 추가된다.( 단 , Resources 의 navigation/your_nav_graph xml 파일은 생성되어 있어야 된다. -->  
  <fragment  
  android:id="@+id/nav_host"  
  android:name="androidx.navigation.fragment.NavHostFragment"  
  android:layout_width="0dp"  
  android:layout_height="0dp"  
  
  
  app:layout_constraintBottom_toBottomOf="parent"  
  app:layout_constraintEnd_toEndOf="parent"  
  app:layout_constraintStart_toStartOf="parent"  
  app:layout_constraintTop_toTopOf="parent"  
  
  app:defaultNavHost="true"  
  app:navGraph="@navigation/your_nav_graph"  
  />  
  
  
  <com.google.android.material.bottomnavigation.BottomNavigationView  
  android:id="@+id/main_bottom_navigation"  
  android:layout_width="match_parent"  
  android:layout_height="wrap_content"  
  app:layout_constraintBottom_toBottomOf="parent"  
  app:layout_constraintEnd_toEndOf="parent"  
  app:layout_constraintStart_toStartOf="parent"  
  app:menu="@menu/menu_bottom_navigation"  
  />  
  
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Navigation Graph에서 사용할 Fragmement를 여러개 만들어준다. 에를 들어 fragment_blank1.xml , fragment_blank2.xml…
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  xmlns:tools="http://schemas.android.com/tools"  
  android:layout_width="match_parent"  
  android:layout_height="match_parent"  
  tools:context=".BlankFragment"  
  >  
  
  <!-- TODO: Update blank fragment layout -->  
  <TextView  
  android:layout_width="match_parent"  
  android:layout_height="match_parent"  
  android:text="@string/hello_blank_fragment"  
  />  
  
</FrameLayout>
  1. 이제 앞서 비어있던 navigation 파일(your_nav_graph.xml) 을 열어서 위에서 만든 fragment를 요소로서 추가해보자.
    디자인 화면(중앙에디터)에서 +기호가 달린 버튼을 누르면 화면에 추가가능한 fragment, activity가 나온다.
    원하는 fragment 를 지정하고, 화면상에서 각각 연결해주면된다.
    app:startDestination="@id/blankFragment2"는 시작시 보여줄 fragment를 지정한다.
 <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/your_nav_graph"  
  app:startDestination="@id/blankFragment2">  
  <fragment  
  android:id="@+id/blankFragment2"  
  android:name="com.sugoigroup.nav11.BlankFragment"  
  android:label="fragment_blank"  
  tools:layout="@layout/fragment_blank" >  
    <action  
  android:id="@+id/action_blankFragment2_to_loginFragment"  
  app:destination="@id/loginFragment" /> 
    
	<argument  
	  android:name="param_count"  
	  app:argType="integer"  
	  android:defaultValue="0" /> 
  </fragment>  
  <fragment  
  android:id="@+id/loginFragment"  
  android:name="com.sugoigroup.nav11.ui.login.LoginFragment"  
  android:label="fragment_login"  
  tools:layout="@layout/fragment_login" >  
    <action  
  android:id="@+id/action_loginFragment_to_mainActivity22"  
  app:destination="@id/otherActivity" />  
  </fragment>  
  <navigation android:id="@+id/otherActivity"  
  app:startDestination="@id/mainActivity22">  
    <activity  
  android:id="@+id/mainActivity22"  
  android:name="com.sugoigroup.nav11.MainActivity2"  
  android:label="activity_main2"  
  tools:layout="@layout/activity_main2" />  
  </navigation>  
</navigation>
  1. activity_main에서 bottomnavigation 에 탭메뉴가 될 것들을 지정안했기 때문에 res 폴더에서 오른쪽버튼 - New - Android Resource File 선택한다음, 대화상자에서 Resource type: Menu 부분을 선택하여 res/menu/your_menu.xml 이 생성되도록 하고 탭아이템이 될 메뉴를 지정한다.
    여기서 중요한것이 android:id="@id/blankFragment2" 와 your_nav_graph.xml 에서 fragment에 지정한ID android:id="@+id/blankFragment2" 가 동일해야 하단탭버튼을 클릭할때 fragment또는 activity가 자동으로 매칭되어 화면이동이 가능해진다.
<menu xmlns:android="http://schemas.android.com/apk/res/android">  
  
  <item  
  android:id="@id/blankFragment2"    
  android:icon="@drawable/ic_android"
  android:title="@string/title_menu_first" />  
  
  <item  
  android:id="@id/loginFragment"  
  android:title="@string/title_menu_second" />  
  
  <item  
  android:id="@id/otherActivity"  
  android:title="@string/title_menu_three" />  
  
</menu>
  1. your_nav_host 를 지정한 레이아웃을 가진 액티비티(여기선 MainActivity) 에서 아래와 같이 네이베이션 사용을 선언하다.
class MainActivity : AppCompatActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_main)  
        
        val your_bottom_navigation = findViewById<BottomNavigationView>(R.id.your_bottom_navigation)  
  
        NavigationUI.setupWithNavController(your_bottom_navigation, findNavController(R.id.your_nav_host))  
    }  
}
  1. 과정4에서 argument 를 지정해주었다. 따라서 BlankFragment 에서 LoginFragment로 화면전이가 될때 Safe Arg 클래스가 자동을 성성되어 바로 사용할수 있다.
<navigation ...>  
/*
  <fragment  
  ...  
  android:name="com.sugoigroup.nav11.BlankFragment"  
  ... >  
    <action  
	  ...
	  app:destination="@id/loginFragment" /> 
	<argument  
	  android:name="param_count"  
	  app:argType="integer"  
	  android:defaultValue="0" /> 
  </fragment>  
  <fragment  
  ...  
  android:name="com.sugoigroup.nav11.ui.login.LoginFragment"  
  ... >  
  </fragment> 
*/

// LoginFragment.kt
  
class LoginFragment : Fragment() {  
  
    val myFromBlankParam by navArgs<BlankFragmentArgs>()
  
	override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
	    super.onViewCreated(view, savedInstanceState)  
	    val myFutureAge = myFromBlankParam.paramCount * 2
    }
}

수동으로 화면이동

버튼을 눌렀을때등 화면이동을 수동으로 할수 있다.
Navigation.findNavController(view).navigate(R.id.액션id또는목적지id);

 // 액션id또는목적지id 는 loginFragment 또는 action_loginFragment_to_mainActivity22 가 될수 있다.
 // action id 를 지정하는게 애니메이션효과등을 출수 있기 때문에 더 좋다.
  <fragment  
  android:id="@+id/loginFragment"  
  android:name="com.sugoigroup.nav11.ui.login.LoginFragment"  
  android:label="fragment_login"  
  tools:layout="@layout/fragment_login" >  
    <action  
  android:id="@+id/action_loginFragment_to_mainActivity22"  
  app:destination="@id/otherActivity" />  
  </fragment> 

기타 옵션

Pop Behavior - popUpToIncursive 옵션을 true 로 하면 현재 화면에서 다음화면으로 넘어갈경우 스택이 쌓지않고 이동하도록 한다.
Pop Behavior - popUpTo 는 쌓여있는 스택을 모두 popup 시키고 마지막 화면만 남아있도록 한다.
Deep link - 외부에서 URI를 통해서 현재 화면에 바로 들어 접속할수 있도록 딥링크주소를 만들수 있다.