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\밑에 있는 파일대로 만들면된다.
      }