How to collect a stateflow from a Custom View? - java

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

cannot find implementation for RoomDatabase while injecting ViewModel into Fragment with Factory

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'

Android - Observation method did not work

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.

How to delete a recyclerrview item from a room database

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()
}
})

LivePagedList emitting empty list

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?

Error injecting dependencies in Fragment using Dagger 2

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
}
}

Categories