[android] GitHubBrowser 의문점

GitHubBrowser 의문점

  1. 어디에 쓰이는 코드인고?

@Suppress("unused")
@Module
abstract class FragmentBuildersModule {
    @ContributesAndroidInjector
    abstract fun contributeRepoFragment(): RepoFragment

    @ContributesAndroidInjector
    abstract fun contributeUserFragment(): UserFragment

    @ContributesAndroidInjector
    abstract fun contributeSearchFragment(): SearchFragment
}

ContributesAndroidInjector?

  1. ActivityBinder
@Module
abstract public class ActivityBinder {

    @ContributesAndroidInjector
    abstract MainActivity bindMainActivity();

    @ContributesAndroidInjector
    abstract DetailActivity bindDetailActivity();
}

우리는 MainActivity, DetailActivity 에 했던 작업들을 전부 모듈을 사용해 처리할려고 합니다. 그러기 위해서 모듈에 MainActivity와 DetailActivity 를 ContributesAndroidInjector 어노테이션을 사용하여 제공

  1. AppCompoent

    Component는 모듈에서 제공받은 객체를 조합하여 어디에 주입할지 정하는 역활

@Singleton
@Component(
        modules = {
                BurgerModule.class,
                ActivityBinder.class,
                AndroidSupportInjectionModule.class
        }
)
/*AndroidSupportInjectionModule -> 대거의 안드로이드 지원모듈 ㅇAndroidInjector<MyApplication>을 상속함으로써 MyApplication 으로 주입합니다.*/
public interface AppComponent extends AndroidInjector<MyApplication> {

}
  1. DaagerAppCompactActivity

    public class MainActivity extends DaggerAppCompatActivity {
       
        @Inject
        Burger burger;
       
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            setTitle("MainActivity");
       
            // DaggerBurgerComponent 함수는 필요 없습니다.
            /* DaggerBurgerComponent.builder().build()
                    .inject(this);*/
       
            Log.d("MyTag","MainActivity burger bun : " 
                    + burger.bun.getBun() + " , patty : " + burger.patty.getPatty());
       
        }
    }
    

    MainActivity, DetailActivity 를 AppCompatActicity 가 아닌 DaggerAppCompatActivity 로 상속해 주면 BurgerComponet 사용 없이 객체를 주입할 수 있게 됩니다.

아하 간단히 쓰려고 저렇게 썻구나 그런데..

class SearchFragment : Fragment(),Injectable{
  ...
  ...
}

애는 뭐 딱히 없는데?

https://github.com/google/dagger/issues/796

img

라고 한다. Thx PhilGlass!!

찾아보니 역시 있다.

@Suppress("unused")
@Module
abstract class MainActivityModule {
    @ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
    abstract fun contributeMainActivity(): MainActivity
}

  1. ViewModelModule

@Suppress("unused")
@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(UserViewModel::class)
    abstract fun bindUserViewModel(userViewModel: UserViewModel): ViewModel

    @Binds
    @IntoMap
    @ViewModelKey(SearchViewModel::class)
    abstract fun bindSearchViewModel(searchViewModel: SearchViewModel): ViewModel

    @Binds
    @IntoMap
    @ViewModelKey(RepoViewModel::class)
    abstract fun bindRepoViewModel(repoViewModel: RepoViewModel): ViewModel

    @Binds
    abstract fun bindViewModelFactory(factory: GithubViewModelFactory): ViewModelProvider.Factory
}

public class MyViewModel extends ViewModel {
 
    private List<MyData> mData = new ArrayList<>();
 
    public List<MyData> getData() {
        return mData;
    }
 
    public void setData(List<MyData> data) {
        mData = data;
    }
}
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mMyViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
}

잠시 옆으로 새자면 그동안 LiveData 예제나 다른 글들을 보면서 궁금했던것은

ViewModelProviders.Factory가 들어갈 때도 있고 안들어갈 때도 있어서 왜 쓰냐는 것이었다.


ViewModel 클래스는 자체적으로 어떤 기능도 포함하고 있지 않기 때문에 일반적인 객체처럼 new 키워드로(자바일 경우) 객체를 생성하는 것은 아무런 의미가 없습니다. 반드시 ViewModelProvider를 통해서 객체를 생성해야지만 HolderFramgnt에 의해 ViewModel이 관리되며, 기기의 구성 변경에서 살아남을 수 있습니다.

이런한 이유로 커스텀 생성자를 갖는 ViewModel은 ViewModelProvider에게 해당 객체를 생성할 수 있는 방법을 제공해야 합니다. 이런 경우를 다루기 위해 ViewModel 라이브러리는 개발자에게 ViewModelProvider.Factory 인터페이스를 사용하도록 강제하고 있습니다.


강제하는데 Factory를 쓰고 안쓰는 코드가 있다. 그리고 google aac 예제는 ViewModelFactory를 di 폴더로 빼놓아서 확장성 있게 사용한다.

// Activity에서 초기화
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
		return of(activity, null);
}

// Activity에서 초기화
public static ViewModelProvider of(@NonNull FragmentActivity activity,
				@Nullable Factory factory) {
		Application application = checkApplication(activity);
		if (factory == null) {
				factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
		}
		return new ViewModelProvider(activity.getViewModelStore(), factory);
}

factory가 없을 경우 내부의 ViewModelProvider.AndroidViewModelFactory.getInstance()를 호출하는 코드가 포함되어있는데, 아래의 AndroidViewModelFactory 코드를 확인할 수 있다. 그리고 밑에 create코드를 보면

public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {

	private static AndroidViewModelFactory sInstance;

	@NonNull
	public static AndroidViewModelFactory getInstance(
			@NonNull Application application) {
		if (sInstance == null) {
			sInstance = new AndroidViewModelFactory(application);
		}
		return sInstance;
	}

	private Application mApplication;

	/**
	 * Creates a {@code AndroidViewModelFactory}
	 *
	 * @param application an application to pass in {@link AndroidViewModel}
	 */
	public AndroidViewModelFactory(@NonNull Application application) {
		mApplication = application;
	}

	@NonNull
	@Override
	public <T extends ViewModel> T create(
			@NonNull Class<T> modelClass) {
		if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
			//noinspection TryWithIdenticalCatches
			try {
				return modelClass.getConstructor(Application.class).newInstance(mApplication);
			} catch (NoSuchMethodException e) {
				throw new RuntimeException("Cannot create an instance of " + modelClass, e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException("Cannot create an instance of " + modelClass, e);
			} catch (InstantiationException e) {
				throw new RuntimeException("Cannot create an instance of " + modelClass, e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException("Cannot create an instance of " + modelClass, e);
			}
		}
		return super.create(modelClass);
	}
}
public static class NewInstanceFactory implements Factory {

	@SuppressWarnings("ClassNewInstance")
	@NonNull
	@Override
	public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
		//noinspection TryWithIdenticalCatches
		try {
			return modelClass.newInstance();
		} catch (InstantiationException e) {
			throw new RuntimeException("Cannot create an instance of " + modelClass, e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Cannot create an instance of " + modelClass, e);
		}
	}
}

다시 본론으로 돌아오면 뷰모델에 생성자가 필요한 경우 팩토리를 ViewModelProvider의 두번째 param으로 사용한다.

class PostListActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post_list)
        getAppInjector().inject(this)
        val vm = ViewModelProviders.of(this, viewModelFactory)[PostListViewModel::class.java]
        vm.posts.observe(this, Observer(::updatePosts))
    }
    
    //...
}

그리고 우리의 GithubBrowserSample 께서는

class SearchViewModel @Inject constructor(repoRepository: RepoRepository) : ViewModel() {

    private val _query = MutableLiveData<String>()
    private val nextPageHandler = NextPageHandler(repoRepository)

    val query : LiveData<String> = _query

    val results: LiveData<Resource<List<Repo>>> = Transformations
        .switchMap(_query) { search ->
            if (search.isNullOrBlank()) {
                AbsentLiveData.create()
            } else {
                repoRepository.search(search)
            }
        }

    val loadMoreStatus: LiveData<LoadMoreState>
        get() = nextPageHandler.loadMoreState

    fun setQuery(originalInput: String) {
        val input = originalInput.toLowerCase(Locale.getDefault()).trim()
        if (input == _query.value) {
            return
        }
        nextPageHandler.reset()
        _query.value = input
    }

Repository 를 생성자로 주입하고 있다. 빙고! Muchas grascias! RepoRepository로 다시들어가보면

@Singleton
@OpenForTesting
class RepoRepository @Inject constructor(
    private val appExecutors: AppExecutors,
    private val db: GithubDb,
    private val repoDao: RepoDao,
    private val githubService: GithubService
) {

    private val repoListRateLimit = RateLimiter<String>(10, TimeUnit.MINUTES)

    fun loadRepos(owner: String): LiveData<Resource<List<Repo>>> {
        return object : NetworkBoundResource<List<Repo>, List<Repo>>(appExecutors) {
            override fun saveCallResult(item: List<Repo>) {
                repoDao.insertRepos(item)
            }

            override fun shouldFetch(data: List<Repo>?): Boolean {
                return data == null || data.isEmpty() || repoListRateLimit.shouldFetch(owner)
            }

            override fun loadFromDb() = repoDao.loadRepositories(owner)
          ...
        }

저런식으로 생성자 주입을 해놓았다. 여담으로 지금 하고 있는 프로젝트에는 언제 적용할지는 모르겠지만 배민같은 경우 인터넷 연결이 끊긴 오프라인상태에서도 저장된 정보를 볼수 있게 구현했다. 나중에 그런 처리를 해줄때 retrofit과 roomDB로 상태값체크후 불러올때 쓰일 것 같다.

3. AbsentLiveData

class AbsentLiveData<T : Any?> private constructor(): LiveData<T>() {
    init {
        // use post instead of set since this can be created on any thread
        postValue(null)
    }

    companion object {
        fun <T> create(): LiveData<T> {
            return AbsentLiveData()
        }
    }
}

참고 사이트들