I am currently learning MVVM architecture.
I tried to make a BaseActivity class.
My BaseActivity:
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
AppCompatActivity(),
EventListener {
lateinit var binding: Binding
private var viewModel: ViewModel? = null
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
binding = DataBindingUtil.setContentView(this, layoutid)
this.viewModel = viewModel ?: getViewModel()
binding.setVariable(getBindingVariable(), viewModel)
binding.executePendingBindings()
}
#get: LayoutRes
abstract val layoutid: Int
abstract fun getViewModel(): ViewModel
abstract fun getBindingVariable(): Int
private fun getViewModelClass(): Class<ViewModel> {
val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
return type as Class<ViewModel>
}
}
Now I am using this BaseActivity in my SplashActivity:
class SplashActivity : BaseActivity<SplashActivityViewModel, ActivitySplashBinding>() {
private lateinit var viewModel: SplashScreenViewModel
override fun onFailure(message: String) {}
override fun onStarted() {}
override fun onSuccess() {}
override fun getViewModel(): SplashActivityViewModel {
viewModel = ViewModelProvider(this).get(SplashActivityViewModel::class.java)
return viewModel
}
override fun getBindingVariable(): Int {
return BR.splash_viewmodel
}
override val layoutid: Int
get() = R.layout.activity_splash
}
I have used following answer as a reference to implement my BaseActivity.kt: https://stackoverflow.com/questions/55289334/how-to-have-generic-viewmodel-in-baseactivty-class
But I am getting a blank white screen while running the app.
Can someone please tell me what is the problem here or how to make this BaseActivity (without using dependency injection)?
you have overridden the wrong onCreate
override fun onCreate(savedInstanceState: Bundle?) {
I did play around with something like that few years ago, you can find my approach here
Related
I have base activity< T : ViewDataBinding , VM : ViewModel > extends AppCompatActivity()
and i initialize view binding and view model but when run the app i get this error "lateinit property dataBinding has not been initialized"
I don't know what I miss or what the wrong
Below is Base Activity Code
open abstract class BaseActivity<T : ViewDataBinding , VM : ViewModel> : AppCompatActivity() {
lateinit var dataBinding : T
lateinit var viewModel : VM
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
dataBinding = getViewBinding()
setContentView(dataBinding.root)
viewModel = generateViewModel()
}
abstract fun getViewBinding(): T
abstract fun generateViewModel(): VM
and this My HomeActivity
class HomeActivity : BaseActivity<ActivityHomeBinding, HomeViewModel>() {
override fun getViewBinding(): ActivityHomeBinding = ActivityHomeBinding.inflate(layoutInflater)
override fun generateViewModel(): HomeViewModel {
return ViewModelProvider(this).get(HomeViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding.vm = viewModel
}
}
this is the error message
Because in your base class the onCreate() with 2 parameters is not going to get called during the activity's lifecycles. And in your subclass you override the onCreate() with a parameter.
Just simple change your base class to override the onCreate() with a parameter to fix the problem. And the other thing is you implement these class the java's way.
You can just make it better this way:
BaseClass
abstract class BaseActivity<T : ViewDataBinding , VM : ViewModel> : AppCompatActivity() {
protected abstract val dataBinding : T
protected abstract val viewModel : VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(dataBinding.root)
}
}
Subclass:
class HomeActivity : BaseActivity<ActivityHomeBinding, HomeViewModel>() {
override val viewModel get() = ViewModelProvider(this).get(HomeViewModel::class.java)
override val dataBinding get() = ActivityHomeBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding.vm = viewModel
}
}
When you are calling super.onCreate(savedInstanceState) in your HomeActivity, the onCreate from android's Activity is called, but not the onCreate from BaseActivity - because it expected second param persistentState.
So you can do this options to fix the issue:
call super method with 2 params in your HomeActivity
class HomeActivity ... {
...
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
dataBinding.vm = viewModel
}
OR
use onCreate with one param in your BaseActivity
open abstract class BaseActivity<T : ViewDataBinding , VM : ViewModel> : AppCompatActivity() {
lateinit var dataBinding : T
lateinit var viewModel : VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
I was making a base class so that all bindings for child will be set in base
I have done till this
abstract class BaseActivity2<B : ViewBinding?, T : BaseViewModel?> : AppCompatActivity() {
private var viewBinding: B? = null
private var baseViewModel: T? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
but am unable to get a way to bind view in oncreat()
generally we bind layout in view binding as
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
but i am looking for generalized way in base activity
You can declare a lambda property in the constructor to create the binding object
abstract class BaseActivity<B : ViewBinding>(val bindingFactory: (LayoutInflater) -> B) : AppCompatActivity() {
private lateinit var binding: B
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = bindingFactory(layoutInflater)
setContentView(binding.root)
}
}
You can also define binding as lazy property
private val binding: B by lazy { bindingFactory(layoutInflater) }
Then you need to override nothing in your activities
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate)
Other answer will also solve problem but I would like to do in a clean way.
My Base Class
abstract class BaseVMActivity<VM : ViewModel, B : ViewBinding> : BaseActivity() {
lateinit var viewModel: VM
lateinit var binding: B
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(getViewModelClass())
binding = getViewBinding()
setContentView(binding.root)
}
private fun getViewModelClass(): Class<VM> {
val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
return type as Class<VM>
}
abstract fun getViewBinding(): B
}
My activity:
class MainActivity : BaseVMActivity<AppViewModel, ActivityMainBinding>() {
override fun getViewBinding() = ActivityMainBinding.inflate(layoutInflater)
}
Now I can directly access viewModel or binding:
fun dummy(){
binding.bvReport.text = viewModel.getReportText()
}
It's cleaner to override binding object getter inside the child activity I think. So:
abstract class VBActivity<VB : ViewBinding> : AppCompatActivity() {
protected abstract val binding: VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
And lets say MainActivity will be something like:
class MainActivity : VBActivity<ActivityMainBinding>() {
override val binding get() = ActivityMainBinding.inflate(layoutInflater)
}
FIXED
SEE BELOW
If a picture is worth 1,000 words, a video is worth, like, a lot of words. Here's a video explanation of the issue.
I've included a video as it makes things much clearer. The problem: When I first load up the fragment containing the list of items with state I need to toggle, I can toggle that state just fine. I send the update to the Room database and the changes are emitted back to my ViewModel, who then dispatches them to the Fragment.
However, when I leave the fragment and come back, the changes are no longer dispatched. I don't know if I'm doing something incredibly stupid or if this is a bug.
I'm also using the Jetpack Navigation components if that's relevant. I'll include code below.
Please let me know if you need to see any other code referenced below and I'll add it to the question.
Thank you very much for your time and consideration.
ShowsFragment
class ShowsFragment : Fragment(), ShowClickListener, Observer<Resource<List<ShowDomainModel>>> {
#Inject
lateinit var factory: ViewModelFactory
#Inject
lateinit var adapter: ShowsAdapter
private lateinit var showsViewModel: ShowsViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
showsViewModel = ViewModelProviders.of(this, factory).get(ShowsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_shows, container, false)!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter.clickListener = this
shows.adapter = adapter
val shows = showsViewModel.getShows()
shows.observe(this, this)
}
override fun onChanged(resource: Resource<List<ShowDomainModel>>) {
Timber.d("onChanged")
when (resource.state) {
State.SUCCESS -> {
adapter.shows = resource.data!!
adapter.notifyDataSetChanged()
}
State.LOADING -> Unit
State.ERROR -> TODO("Handle error state in ShowsFragment")
}
}
override fun onShowFavoriteClicked(show: ShowDomainModel) {
if (show.favorite) {
showsViewModel.unfavoriteShow(show.playlistId)
} else {
showsViewModel.favoriteShow(show.playlistId)
}
}
override fun onShowClicked(show: ShowDomainModel) {
findNavController().navigate(
ShowsFragmentDirections.showEpisodes(show.name, show.playlistId)
)
}
}
ShowsDao
#Dao
abstract class ShowsDao {
#Query("SELECT * FROM $TABLE_NAME")
abstract fun getShows(): Observable<List<ShowCacheModel>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertShows(shows: List<ShowCacheModel>)
#Query("SELECT * from $TABLE_NAME WHERE favorite = 1")
abstract fun getFavoriteShows(): Observable<List<ShowCacheModel>>
#Query("UPDATE $TABLE_NAME SET favorite = :favorite WHERE $COLUMN_SHOW_ID = :showId")
abstract fun setFavorite(showId: String, favorite: Boolean)
}
ShowsViewModel
#Singleton
class ShowsViewModel #Inject constructor(
private val getShows: GetShows,
private val addShowToFavorites: AddShowToFavorites,
private val removeShowFromFavorites: RemoveShowFromFavorites
) : ViewModel() {
private val shows: MutableLiveData<Resource<List<ShowDomainModel>>> = MutableLiveData()
init {
shows.postValue(Resource.loading())
getShows.execute(GetShowsObserver())
}
override fun onCleared() {
getShows.dispose()
super.onCleared()
}
fun getShows(): LiveData<Resource<List<ShowDomainModel>>> = shows
fun favoriteShow(id: String) = addShowToFavorites.execute(
AddShowToFavoritesObserver(),
AddShowToFavorites.Params.forShow(id)
)
fun unfavoriteShow(id: String) = removeShowFromFavorites.execute(
RemoveShowFromFavoritesObserver(),
RemoveShowFromFavorites.Params.forShow(id)
)
inner class GetShowsObserver : DisposableObserver<List<ShowDomainModel>>() {
override fun onComplete() {
Log.d("ShowsViewModel","onComplete")
throw RuntimeException("GetShows should not complete, should be observing changes to data.")
}
override fun onNext(showList: List<ShowDomainModel>) {
shows.postValue(Resource.success(showList))
}
override fun onError(e: Throwable) {
shows.postValue(Resource.error(e.localizedMessage))
}
}
inner class AddShowToFavoritesObserver : DisposableCompletableObserver() {
override fun onComplete() = Unit
override fun onError(e: Throwable) =
shows.postValue(Resource.error(e.localizedMessage))
}
inner class RemoveShowFromFavoritesObserver : DisposableCompletableObserver() {
override fun onComplete() = Unit
override fun onError(e: Throwable) =
shows.postValue(Resource.error(e.localizedMessage))
}
}
Turns out the fix is very simple. I just needed to use the Activity as the lifecycle that was passed to the ViewModel.
ShowsFragment
// ...
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
// CHANGED ONE LINE AND IT WORKS
// (changed this to activity!!)
// CHANGED ONE LINE AND IT WORKS
showsViewModel = ViewModelProviders.of(activity!!, factory).get(ShowsViewModel::class.java)
}
// ...
}
Trying to use Dagger with Kotlin on Android. And got the exception:
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
at com.ad.eartquakekotlin.main.MainFragment.onViewCreated(MainFragment.kt:43)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1471)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:802)
at ...
The app is special for testing. I get earthquakes and show them on a device screen.
All I want is
1. Inject the presenter in my Fragment (View)
2. Inject the api in my presenter
There is the structure of my project:
There are two modules and components there, as you can see:
#Module
class ApplicationModule(private val application: Application) {
#Provides
#Singleton
fun provideApplication():Application = application
}
#Module
class MainModule (private val view: MainContract.View) {
#Provides
fun provideView(): MainContract.View {
return view
}
#Provides
fun providePresenter(): MainContract.Presenter {
return MainPresenter(view)
}
}
And components:
#Component(modules = [ApplicationModule::class])
interface ApplicationComponent {
fun inject(application: Application)
fun plus (mainModule: MainModule) : MainComponent
}
and
#Subcomponent(modules = [MainModule::class])
interface MainComponent {
fun inject (view : MainContract.View)
}
There is a contract:
interface MainContract {
interface View {
fun showLoading()
fun hideLoading()
fun showMessage(message: String)
fun showData(data: EarthquakeRootObject)
}
interface Presenter {
fun onDestroy()
fun loadData()
}
}
Application class:
class MainApp: Application() {
companion object {
lateinit var graph: ApplicationComponent
}
override fun onCreate() {
super.onCreate()
buildGraph()
}
private fun buildGraph() {
graph = DaggerApplicationComponent
.builder()
.applicationModule(ApplicationModule(this))
.build()
}
}
Fragment (where I want to use Injection)
class MainFragment : Fragment(), MainContract.View {
private lateinit var earthquakesAdapter: EarthquakeRecyclerViewAdapter
private lateinit var earthquakes: EarthquakeRootObject
#Inject lateinit var presenter: MainContract.Presenter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return container?.inflate(R.layout.fragment_main)
}
override fun onAttach(context: Context?) {
super.onAttach(context)
MainApp.graph.plus(MainModule(this)).inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
earthquakesRecyclerView.layoutManager = LinearLayoutManager(context)
earthquakesRecyclerView.setHasFixedSize(true)
presenter.loadData()
}
And my presenter
class MainPresenter (var view: MainContract.View?) : MainContract.Presenter {
private var disposable: Disposable? = null
#Inject lateinit var api : EarthquakeApi
override fun onDestroy() {
disposable?.dispose()
view = null
}
override fun loadData() {
view?.showLoading()
disposable = api.getEarthquakes()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
response ->
view?.showData(response)
view?.hideLoading()
},
{
throwable ->
view?.showMessage(throwable.message ?: "Ошибка")
view?.hideLoading()
}
)
}
What do I do wrong?
You need to replace
fun inject (view : MainContract.View)
with
fun inject(target : MainFragment)
I have a BaseActivity, which has Dagger behavior inside:
abstract class BaseActivity : DaggerAppCompatActivity(), HasSupportFragmentInjector {
#Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState, persistentState)
/**
* Method that gets called after the [onCreate] method
*/
public override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
applyDebugOverlay(this)
}
override fun supportFragmentInjector() = fragmentDispatchingAndroidInjector
}
Now I want to test, that applyDebugOverlay(this) is calling. For this, I use Robolectric in my AbstractActivityTest
#Test
fun testDebugOverlayIsShown() {
underTest.onPostCreate(Bundle())
val buildTimeStamp = underTest.inc_debugOverlay_tv_date.text
assert(buildTimeStamp.isNotEmpty())
}
But I get "No injector factory bound for Class" error, and it is not possible to bind BaseActivity with #ContributesAndroidInjector in my ActivityBuilder Class (would get another error).