This article explains how to use ViewModels in custom views :
class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.summaryModel.observe(findViewTreeLifecycleOwner()!!, ::populateSummaryView)
}
}
It works well, but how can I use this technique using StateFlow instead of LiveData?
How can I do this without having a reference to the fragment?
class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.summaryModel.collect {
}
}
}
}
}
Related
I am struggling with viewmodel injection. I have been following tutorials and changed the code a little bit in order to adjust it to my needs, but the app crashes.
I have App class holding my DaggerComponent with it's modules. Inside it's onCreate I have:
component = DaggerAppComponent.builder().daoModule(DaoModule(this)).build()
My AppModule:
#Singleton
#Component(modules = [DaoModule::class, ViewModelModule::class])
interface AppComponent {
val factory: ViewModelFactory
}
ViewModelModule :
#Module
abstract class ViewModelModule {
#Binds
#Singleton
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#Singleton
#IntoMap
#ViewModelKey(TaskViewModel::class)
abstract fun splashViewModel(viewModel: TaskViewModel): ViewModel
}
MyFactory:
#Singleton
class ViewModelFactory #Inject constructor(
private val viewModels: MutableMap<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModels[modelClass]?.get() as T
}
I used here ViewModelKey, ViewModelModule and Factory, and Fragment extension function to perform Fragment viewmodel injection. I found it online and used it succesfuly on previous projects. This is my util function:
#MainThread
inline fun <reified VM : ViewModel> Fragment.daggerViewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this }
) = createViewModelLazy(
VM::class,
{ ownerProducer().viewModelStore },
{ App.component.factory }
)
And my DaoModule.
#Module
class DaoModule(private val app: Application) {
#Provides
#Singleton
fun getDB(): TaskDatabase = TaskDatabase.getAppDatabase(context())
#Provides
#Singleton
fun context(): Context = app.applicationContext
#Provides
fun gettaskDao(taskDatabase: TaskDatabase) : TaskDao = taskDatabase.TaskDao()
}
My entity:
#Entity(tableName = "userinfo")
data class Task(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") val id: Int = 0,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "email") val email: String,
#ColumnInfo(name = "phone") val phone: String?
)
My TaskDatabase as follows:
#Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
abstract fun TaskDao(): TaskDao
companion object {
private var INSTANCE: TaskDatabase? = null
fun getAppDatabase(context: Context): TaskDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.applicationContext, TaskDatabase::class.java, "AppDBB"
)
.allowMainThreadQueries()
.build()
}
return INSTANCE!!
}
}
}
My Dao interface.
#Dao
interface TaskDao {
#Query("SELECT * FROM userinfo")
fun getAllTaskInfo(): List<Task>?
#Insert
fun insertTask(user: Task?)
#Delete
fun deleteTask(user: Task?)
#Update
fun updateTask(user: Task?)
}
And now I have a logic to init my TaskViewModel inside my Fragment and attach observer to my Task List. However the app crashes.
Inside my fragment I have:
val viewModel: TaskViewModel by daggerViewModels { requireActivity() }
and also:
DaggerFragmentComponent
.builder()
.appComponent((requireActivity().application as App).getAppComponent())
.build()
.inject(this)
viewModel.allTaskList.observe(viewLifecycleOwner) {
// textView.text = it.toString()
}
and my TaskViewModel class is as follows:
class TaskViewModel #Inject constructor(var taskDao: TaskDao) : ViewModel() {
private var _allTaskList = MutableLiveData<List<Task>>()
val allTaskList = _allTaskList as LiveData<List<Task>>
init {
getAllRecords()
}
private fun getAllRecords() = _allTaskList.postValue(taskDao.getAllTaskInfo())
fun insertTask(task: Task) {
taskDao.insertTask(task)
getAllRecords()
}
}
Now I understand that this is A LOT of code, but can somebody help me figure this out? The dagger sees it's graph as I can build the project, so all the dependencies are provided. What I did wrong here? My logcat:
I found the solution myself. This was missing.
implementation 'androidx.room:room-runtime:2.5.0-alpha01'
The observer I created is not working. There are classes that I have run before, but the current one did not work. I could not understand where the problem was. The observation methods I created before are working.
SingleLiveData.kt
class SingleLiveData<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Timber.w("Multiple observers registered but only one will be notified of changes.")
}
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
}
ViewModel.kt
class ExampleViewModel#Inject constructor(): ViewModel() {
val event = SingleLiveData<ExampleViewEvent>()
fun goToHome(userId: Long) {
event.postValue(ExampleViewEvent.GoToHome(userId))
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observe(viewModel.event, ::onViewEvent)
}
private fun onViewEvent(viewEvent: ExampleViewEvent) {
when (viewEvent) {
is ExampleViewEvent.GoToHome ->
findNavController().navigate(
ExampleFragmentDirections
.actionExampleFragmentToHomeFragment(viewEvent.id))
}
}
EDIT:
observe() Method:
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, observer: (T) -> Unit) {
liveData.observe(this, Observer {
it?.let { t -> observer(t) }
})
}
fun <T> LifecycleOwner.observe(liveData: MutableLiveData<T>, observer: (T) -> Unit) {
liveData.observe(this, Observer {
it?.let { t -> observer(t) }
})
}
NOTE: hasActiveObservers() return false.
I have a program that I am currently working on. I need to make it so when I hit the "delete" button after typing the food name in, it deletes the recycler view item from the list and database. Here's a screen shot of what I mean:
I have copy and pasted some of the main structures that I know I need to modify. I just do not know what exactly I need to do. These sections are commented out.
Here is the code in the MainActivity.kt:
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private var adapter: FoodListAdapter? = null
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
adapter = FoodListAdapter(R.layout.food_row)
food_recycler.layoutManager = LinearLayoutManager(this)
food_recycler.adapter = adapter
buttonAdd.setOnClickListener { viewModel.insertFood(Food(editFoodName.text.toString())) }
// buttonDelete.setOnClickListener { viewModel.insertFood(Food(editFoodName.text.toString())) }
viewModel.allFoods?.observe(this, Observer { foods ->
foods?.let {
adapter?.setFoodList(it)
}
})
}
}
The code in FoodDao.kt:
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
#Dao
interface FoodDao {
#Insert
fun insertFood(product: Food)
#Query("SELECT * FROM foods")
fun getAllFoods(): LiveData<List<Food>>
#Query ("SELECT * FROM foods WHERE foodName = :name")
fun findFood(name: String) : List<Food>
}
The code in MainViewModel.kt:
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val repository: FoodRepository =
FoodRepository(application)
var allFoods: LiveData<List<Food>>?
init {
allFoods = repository.allFoods
}
fun insertFood(food: Food) {
repository.insertFood(food)
}
// fun deleteFood(food: Food) {
// repository.deleteFood(food)
// }
}
The code in FoodRepository.kt:
import android.app.Application
import android.os.AsyncTask
import androidx.lifecycle.LiveData
class FoodRepository(application: Application) {
val allFoods: LiveData<List<Food>>?
private var foodDao: FoodDao?
init {
val db: FoodDatabase? =
FoodDatabase.getDatabase(application)
foodDao = db?.foodDao()
allFoods = foodDao?.getAllFoods()
}
fun insertFood(newfood: Food) {
val task = InsertAsyncTask(foodDao)
task.execute(newfood)
}
private class InsertAsyncTask constructor(private val asyncTaskDao: FoodDao?) :
AsyncTask<Food, Void, Void>() {
override fun doInBackground(vararg params: Food): Void? {
asyncTaskDao?.insertFood(params[0])
return null
}
}
// fun deleteFood(newfood: Food) {
// val task = DeleteAsyncTask(foodDao)
// task.execute(newfood)
// }
//
// private class DeleteAsyncTask constructor(private val asyncTaskDao: FoodDao?) :
// AsyncTask<Food, Void, Void>() {
//
// override fun doInBackground(vararg params: Food): Void? {
// asyncTaskDao?.insertFood(params[0])
// return null
// }
// }
}
The thing here is that you need to update your recycler view with your new data:
recyclerView?.notifyDataSetChanged()
first you set the delete method setup.
#Delete
fun deleteFood(product: Food);
your view model method is right and repository method is right.you call method in activity.
buttonDelete.setOnClickListener { viewModel.deleteFood(Food(editFoodName.text.toString())) }
When you call the all foods then you need to update your recycler view with your new data.
viewModel.allFoods?.observe(this, Observer { foods ->
foods?.let {
adapter?.setFoodList(it)
recyclerView?.notifyDataSetChanged()
}
})
I am using a paging library from Android Architecture Components.
Paging is implemented using ItemKeyedDataSource
class MyDatasource(
private val queryMap: HashMap<String, String>) : ItemKeyedDataSource<String, Order>() {
private val compositeDisposable: CompositeDisposable by lazy { CompositeDisposable() }
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<Order>) {
compositeDisposable.add(
MyService.getService().fetchData(queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<OrdersResponse>() {
override fun onNext(orders: OrdersResponse) {
callback.onResult(orders.data)
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onComplete() {
}
})
)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Order>) {
// do nothing
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Order>) {
queryMap["offsetOrderId"] = params.key
compositeDisposable.add(
MyService.getService().fetchData(queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<OrdersResponse>() {
override fun onNext(orders: OrdersResponse) {
callback.onResult(orders.data)
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
}
})
)
}
override fun getKey(item: Order): String {
return item.orderId
}
}
I build pagedlist in my viewmodel
class MyViewModel() : ViewModel() {
private var myPagingConfig: PagedList.Config? = null
var dataList: LiveData<PagedList<Order>>? = null
fun getOrders(params: HashMap<String, String>) {
if (myPagingConfig == null) {
myPagingConfig = PagedList.Config.Builder()
.setPageSize(LIMIT)
.setPrefetchDistance(10)
.setEnablePlaceholders(false)
.build()
}
dataList = LivePagedListBuilder(MyDataFactory(
MyDatasource(params)), myPagingConfig!!)
.setInitialLoadKey(null)
.setFetchExecutor(Executors.newFixedThreadPool(5))
.build()
}
}
However, when I observe the dataList in my activity, it sometimes (most of the times) returns an empty list, while in logcat I see that I had fetched data successfully. callback.onResult is invoked after it returns an empty list, but observer never gets notified again.
Can you tell me if what would cause this?
I am trying to use dagger 2 to inject dependencies in a fragment in my application
I have the following modules
Network Module
#Module(includes = ContextModule.class)
public class NetworkModule {
#Provides
Cache getCacheFile(Context context) {
File cacheFile = new File(context.getCacheDir(), "okhttp-cache");
return new Cache(cacheFile, 10 * 1000 * 1000);
}
#Provides
HttpLoggingInterceptor getHttpLoggingInteceptor() {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
return logging;
}
#Provides
OkHttp3Downloader getOkHttp3Downloader(Context context) {
return new OkHttp3Downloader(context);
}
#Provides
OkHttpClient getOkHttpClient(HttpLoggingInterceptor interceptor, Cache cache) {
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newBuilder().cache(cache)
.addInterceptor(interceptor)
.build();
return okHttpClient;
}
#Provides
Retrofit getRetrofit(OkHttpClient client, GsonConverterFactory gsonConverterFactory, RxJava2CallAdapterFactory callAdapter) {
return new Retrofit.Builder()
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(callAdapter)
.baseUrl("https://api.themoviedb.org/3/")
.client(client)
.build();
}
#Provides
GsonConverterFactory getGsonConverterFactory() {
return GsonConverterFactory.create();
}
#Provides
RxJava2CallAdapterFactory getRxJavaFactory() {
return RxJava2CallAdapterFactory.create();
}
}
ActivityBuilder Module
#Module
public abstract class ActivityBuilder {
#ContributesAndroidInjector
abstract NewsListActivity bindMoviesListActivity();
#ContributesAndroidInjector(modules = NewsListFragmentModule.class)
abstract NewsListFragment bindNewsListFragment();
}
Here is the AppComponent
#Component(modules = {AndroidSupportInjectionModule.class, ContextModule.class, ActivityBuilder.class})
public interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
Builder application(Application application);
AppComponent build();
}
void inject(NewsApp app);
}
This is my activity class where the fragment is embedded
class NewsListActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, HasSupportFragmentInjector {
#Inject
lateinit var mAndroidInjector: AndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_news_list)
setSupportActionBar(toolbar)
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.frame_main, NewsListFragment(), "news-list-fragment")
.commit()
}
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return mAndroidInjector
}
}
And finally this is my Fragment class
class NewsListFragment : Fragment() {
lateinit var picasso: Picasso
#Inject
lateinit var newsService: NewsService
#Inject
lateinit var newsFragmentViewModel: NewsFragmentViewModel
lateinit var newsListAdapter: NewsListAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_news_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
swipe_layout.setColorSchemeColors(resources.getColor(R.color.orange),resources.getColor(R.color.green),resources.getColor(R.color.blue))
newsListAdapter = NewsListAdapter(newsItemClickListener, picasso)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
rv_news_top_headlines.layoutManager = layoutManager
rv_news_top_headlines.setHasFixedSize(true)
rv_news_top_headlines.adapter = newsListAdapter
newsFragmentViewModel = ViewModelProviders.of(this).get(NewsFragmentViewModel::class.java)
swipe_layout.isRefreshing = true
newsFragmentViewModel.init(newsService)
loadDataFromApi()
swipe_layout.setOnRefreshListener {
loadDataFromApi()
}
}
private fun loadDataFromApi() {
// Load data from API
}
override fun onAttach(context: Context?) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
private val newsItemClickListener = fun(article: Articles) {
toast(article.title.toString())
}
}
I followed many different tutorials to find out how to use the new AndroidInjection in Fragments so I don't have the links of those tutorials?
Is there anything I'm doing wrong?
The error
e: /Users/sriramr/Desktop/android/NewsApp/app/src/main/java/in/sriram/newsapp/di/AppComponent.java:12: error: [dagger.android.AndroidInjector.inject(T)] dagger.android.AndroidInjector<android.support.v4.app.Fragment> cannot be provided without an #Provides- or #Produces-annotated method.
public interface AppComponent {
^
dagger.android.AndroidInjector<android.support.v4.app.Fragment> is injected at
in.sriram.newsapp.ui.newslist.NewsListActivity.mAndroidInjector
in.sriram.newsapp.ui.newslist.NewsListActivity is injected at
dagger.android.AndroidInjector.inject(arg0)
Try to change following in NewsListActivity
AndroidInjector<Fragment>
to:
DispatchingAndroidInjector<Fragment>
Also You haven't posted your application class. Make sure it has following things:
class NewsApp : Application(), HasActivityInjector {
#Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun onCreate() {
super.onCreate()
DaggerAppComponent.builder()
.application(this)
.build()
.inject(this)
// some other initialization
}
fun activityInjector(): AndroidInjector<Activity>? {
return activityDispatchingAndroidInjector
}
}