What am I doing wrong in this code, it doesn't release, it continues to work until I close the application.
package com.powermanager.powermanager
import android.content.Context
import android.os.PowerManager
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** PowermanagerPlugin */
class PowermanagerPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var applicationContext: Context
private lateinit var channel : MethodChannel
private lateinit var powerManager: PowerManager
private lateinit var lock: PowerManager.WakeLock
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = flutterPluginBinding.getApplicationContext();
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "powermanager")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(#NonNull call: MethodCall, #NonNull result: Result) {
if (call.method == "toogle") {
powerManager = applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if(powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
lock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,"PROXIMITYSCREENOFFWAKELOCK")
lock.setReferenceCounted(false)
if(lock.isHeld) {
lock.release(1)
result.success(true)
} else {
lock.acquire()
result.success(true)
}
} else {
result.error("notSupported", "PowerManager PROXIMITY_SCREEN_OFF_WAKE_LOCK is notSupported", null)
}
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
Related
The app must show all product but show only last (number 6). when i launch app, work, but return this error : E/RecyclerView: No adapter attached; skipping layout. I'm beginner in android language and searching online i didn't find nothing.
the app was composed from mainActivity and CartActivity
output
this is realtime DB
this is main code
package com.example.tpsi_maccagnola
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import com.example.tpsi_maccagnola.events.UpdateCarrello
import com.example.tpsi_maccagnola.listener.cartlistener
import com.example.tpsi_maccagnola.listener.foodListener
import com.example.tpsi_maccagnola.modelli.Carrello
import com.example.tpsi_maccagnola.modelli.Food
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
import kotlinx.android.synthetic.main.activity_main.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class MainActivity : AppCompatActivity(), foodListener, cartlistener {
lateinit var foodListener: foodListener
lateinit var cartlistener1: cartlistener
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
if (EventBus.getDefault().hasSubscriberForEvent(UpdateCarrello::class.java))
EventBus.getDefault().removeStickyEvent(UpdateCarrello::class.java)
EventBus.getDefault().unregister(this)
}
#Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateCart(event: UpdateCarrello) {
counCartDB()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
caricaDB()
counCartDB()
}
private fun counCartDB() {
val cartmodels: MutableList<Carrello> = ArrayList()
FirebaseDatabase.getInstance()
.getReference("carrello")
.child("UNIQUE_USER_ID")
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (cartsnapshot in snapshot.children) {
val cartmodel = cartsnapshot.getValue(Carrello::class.java)
cartmodel!!.key = cartsnapshot.key
cartmodels.add(cartmodel)
}
cartlistener1.onCartSuccess(cartmodels)
}
override fun onCancelled(error: DatabaseError) {
cartlistener1.onCartFailed(error.message)
}
})
}
private fun caricaDB() {
val foods: MutableList<Food> = ArrayList()
FirebaseDatabase.getInstance()
.getReference("Food").addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
for (FoodSnapshot in snapshot.children) {
val foodModel = FoodSnapshot.getValue(Food::class.java)
foodModel!!.key = FoodSnapshot.key
foods.add(foodModel)
}
foodListener.onloadsuccess(foods)
} else {
foodListener.onloadfailed("non esiste")
}
}
override fun onCancelled(error: DatabaseError) {
foodListener.onloadfailed(error.message)
}
})
}
private fun init() {
foodListener = this
cartlistener1 = this
val gridLayoutManager = GridLayoutManager(this, 2)
food_recycler.layoutManager = gridLayoutManager
food_recycler.addItemDecoration(decoration())
btnCart.setOnClickListener{ startActivity(Intent(this,CartActivity::class.java))}
}
override fun onloadsuccess(foodModelList: List<Food>?) {
val adapter = FoodAdapter(this, foodModelList!!, cartlistener1)
food_recycler.adapter = adapter
}
override fun onloadfailed(message: String?) {
Snackbar.make(layoutprincipale, message!!, Snackbar.LENGTH_LONG).show()
}
override fun onCartSuccess(carrelloList: List<Carrello>) {
var cartTot = 0
for (cartlist in carrelloList!!) {
cartTot += cartlist!!.quantita
/*badge!!.setNumber(cartTot)*/
}
}
override fun onCartFailed(message: String?) {
Snackbar.make(layoutprincipale, message!!, Snackbar.LENGTH_LONG).show()
}
}
this is adapter
package com.example.tpsi_maccagnola
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.tpsi_maccagnola.events.UpdateCarrello
import com.example.tpsi_maccagnola.listener.RecyclerListener
import com.example.tpsi_maccagnola.listener.cartlistener
import com.example.tpsi_maccagnola.modelli.Carrello
import com.example.tpsi_maccagnola.modelli.Food
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
import org.greenrobot.eventbus.EventBus
class FoodAdapter(
private val context: Context,
private val list: List<Food>,
private val cartlistener1: cartlistener
) : RecyclerView.Adapter<FoodAdapter.FoodHolder>() {
class FoodHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
var imageView: ImageView?= null
var txtName: TextView?= null
var txtprice: TextView?= null
private var clickListener: RecyclerListener? = null
fun setClickListener (clickListener: RecyclerListener){
this.clickListener = clickListener;
}
init {
imageView = itemView.findViewById(R.id.imageView) as ImageView;
txtName = itemView.findViewById(R.id.txtName) as TextView;
txtprice = itemView.findViewById(R.id.txtprice) as TextView;
itemView.setOnClickListener(this)
}
override fun onClick(p0: View?) {
clickListener!!.onclcickProdotto(p0,adapterPosition )
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FoodHolder {
return FoodHolder(LayoutInflater.from(context).inflate(R.layout.layout_food,parent,false))
}
override fun onBindViewHolder(holder: FoodHolder, position: Int) {
Glide.with(context).load(list[position].image).into(holder.imageView!!)
holder.txtName!!.text =StringBuilder().append(list[position].name)
holder.txtprice!!.text =StringBuilder("€").append(list[position].price)
holder.setClickListener(object: RecyclerListener{
override fun onclcickProdotto(view: View?, position: Int) {
aggiungiCarrello(list[position])
}
} )
}
private fun aggiungiCarrello(food: Food) {
val utenteCart = FirebaseDatabase.getInstance().getReference("Cart").child("UNIQUE_USER_ID")
utenteCart.child(Food.key!!).addListenerForSingleValueEvent(object :ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
if(snapshot.exists()){ //se è gia presente aggiorno
val cartmodel = snapshot.getValue(Carrello::class.java)
val updatecart: MutableMap<String,Any> = HashMap()
cartmodel!!.quantita = cartmodel!!.quantita+1
updatecart["quantita"] = cartmodel!!.quantita
updatecart["totale"] =cartmodel!!.quantita * cartmodel.price!!.toFloat()
utenteCart.child(food.key!!)
.updateChildren(updatecart)
.addOnSuccessListener {
EventBus.getDefault().postSticky(UpdateCarrello())
cartlistener1.onCartFailed("aggiunto correttamente")
}.addOnFailureListener{e -> cartlistener1.onCartFailed(e.message)
}
}else{
val carrello1 = Carrello
carrello1.key = food.key
carrello1.name = food.name
carrello1.image = food.image
carrello1.price = food.price
carrello1.quantita = 1
carrello1.totale = food.price!!.toFloat()
utenteCart.child(food.key!!)
.setValue(carrello1)
.addOnSuccessListener {
EventBus.getDefault().postSticky(UpdateCarrello())
cartlistener1.onCartFailed("aggiunto correttamente")
}.addOnFailureListener{e -> cartlistener1.onCartFailed(e.message)
}
}
}
override fun onCancelled(error: DatabaseError) {
cartlistener1.onCartFailed(error.message)
}
})
}
override fun getItemCount(): Int {
return list.size
}
}
I want to display real-time info from my Observer.kt class that shows the apps lifecycle ex. "On.Create", "STARTED", "OnStart", etc on my textView in my Main_fragment.xml. I have already built a Owner and Observer class:
package com.example.lifecycleaware import androidx.lifecycle.Lifecycleimport androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry class Owner: LifecycleOwner {
private val lifecycleRegistry: LifecycleRegistry
init {
lifecycleRegistry = LifecycleRegistry(this)
lifecycle.addObserver(Observer())
}
fun startOwner() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
fun stopOwner() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
and also:
package com.example.lifecycleaware import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.OnLifecycleEvent class Observer: LifecycleObserver{
private val LOG_TAG = "Observer"
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
Log.i(LOG_TAG, "onResume")
}
#OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
Log.i(LOG_TAG, "onPause")
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
Log.i(LOG_TAG, "onCreate")
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
Log.i(LOG_TAG, "onStart")
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
Log.i(LOG_TAG, "onStop")
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
Log.i(LOG_TAG, "onDestroy")
}
#OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
Log.i(LOG_TAG, owner.lifecycle.currentState.name)
}
}
I am trying to build an app that uses the youtube API. I need to call the search API, whenever the user searches for something and then display the results in a new activity(SearchResultsActivity). However the scenario I get is that my app, rather than opening the SearchResultsActivity gets redirected to the MainActivity.
The scenario is recorded, and can be better understood through this https://drive.google.com/file/d/1WESzXbRbjhmKCY1gfUq1d2gllIEsILCf/view?usp=sharing.
I want to know what is causing the SearchResultsActivity to be paused by itself and MainActivity being restarted instead. I tried logging the lifecycles to know what was happening and I found this:
Also Search box activity here means the SearchActivity:
package com.example.mytube.UI
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.mytube.R
import com.example.mytube.adapters.SearchedHistoryAdapter
import com.example.mytube.db.SearchDatabase
import com.example.mytube.repository.VideosRepository
class SearchActivity : AppCompatActivity() {
lateinit var searchAdapter: SearchedHistoryAdapter
lateinit var viewModel: VideosViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
val repository = VideosRepository(SearchDatabase.getSearchDatabase(this))
val viewModelFactory = VideosViewModelProviderFactory(repository)
viewModel = ViewModelProvider(this,viewModelFactory).get(VideosViewModel::class.java)
val recyclerView = findViewById<RecyclerView>(R.id.searched_terms)
searchAdapter = SearchedHistoryAdapter()
recyclerView.apply {
adapter = searchAdapter
layoutManager = LinearLayoutManager(this#SearchActivity)
}
val backButton = findViewById<ImageView>(R.id.ivGoback)
backButton.apply {
setOnClickListener {
finish()
}
}
val searchBar = findViewById<EditText>(R.id.search_box)
val searchBtn = findViewById<ImageView>(R.id.ivSearch)
searchBtn.setOnClickListener {
if (searchBar.text.toString().isNotEmpty()) {
viewModel.insertSearchItem(searchBar.text.toString())
val intent = Intent(applicationContext, SearchResultsActivity::class.java)
intent.putExtra("searchQuery", searchBar.text)
startActivity(intent)
}
}
searchBar.setOnKeyListener(View.OnKeyListener{v, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
if (searchBar.text.toString().isNotEmpty()) {
viewModel.insertSearchItem(searchBar.text.toString())
}
return#OnKeyListener true
}
return#OnKeyListener false
})
viewModel.getSearchHistory().observe(this, Observer { searchItems ->
searchAdapter.differ.submitList(searchItems)
})
}
override fun onPause() {
super.onPause()
Log.d("searched", "Search box activity paused")
}
override fun onStop() {
super.onStop()
Log.d("searched", "Search box activity stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("searched", "Search box activity destroyed")
}
}
The SearchResultsActivity:
package com.example.mytube.UI
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AbsListView
import android.widget.ProgressBar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.mytube.R
import com.example.mytube.adapters.VideosAdapter
import com.example.mytube.db.SearchDatabase
import com.example.mytube.repository.VideosRepository
import com.example.mytube.util.Resource
class SearchResultsActivity : AppCompatActivity() {
lateinit var viewModel: VideosViewModel
lateinit var videosAdapter: VideosAdapter
var searchQuery = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search_results)
searchQuery = intent.getStringExtra("searchQuery").toString()
val repository = VideosRepository(SearchDatabase.getSearchDatabase(this))
val viewModelFactory = VideosViewModelProviderFactory(repository)
viewModel = ViewModelProvider(this,viewModelFactory).get(VideosViewModel::class.java)
videosAdapter = VideosAdapter(viewModel)
val recyclerView = findViewById<RecyclerView>(R.id.videos)
recyclerView.apply {
layoutManager = LinearLayoutManager(this#SearchResultsActivity)
adapter = videosAdapter
addOnScrollListener(this#SearchResultsActivity.scrollListener)
}
val progressBar = findViewById<ProgressBar>(R.id.paginationProgressBar)
viewModel.getSearchResults(searchQuery)
viewModel.searchResults.observe(this, Observer { resource ->
when(resource) {
is Resource.Success -> {
hideProgressBar(progressBar)
resource.data?.let { videoResponse ->
if (viewModel.nextSearchPageId != videoResponse.nextPageToken) {
viewModel.nextSearchPageId = videoResponse.nextPageToken
viewModel.searchedVideos.addAll(videoResponse.items)
// videoResponse.items.forEach { viewModel.getChannel(it.snippet.channelId) }
Log.d("Channels", viewModel.channels.toString())
videosAdapter.differ.submitList(viewModel.searchedVideos.toList())
}
else {
Log.d("Videos", "next token dekh ${viewModel.nextPageId}")
videosAdapter.differ.submitList(viewModel.searchedVideos.toList())
}
}
}
is Resource.Error -> {
hideProgressBar(progressBar)
Log.e("Videos", resource.message.toString())
}
is Resource.Loading -> {
showProgressBar(progressBar)
}
}
})
viewModel.channelResponse.observe(this, Observer { resource ->
when (resource) {
is Resource.Success -> {
resource.data?.let { channels ->
viewModel.channels.set(channels.items[0].id, channels.items[0])
// videosAdapter.differ.submitList(viewModel.videos.toList())
}
}
is Resource.Error -> {
Log.e("Channels", resource.message.toString())
}
is Resource.Loading -> {
Log.d("Channels", "Waiting")
}
}
})
}
private fun hideProgressBar(bar: ProgressBar) {
bar.visibility = View.INVISIBLE
isLoading = false
}
private fun showProgressBar(bar: ProgressBar){
bar.visibility = View.VISIBLE
isLoading = true
}
var isScrolling = false
var isLoading = false
val scrollListener = object: RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val manager = recyclerView.layoutManager as LinearLayoutManager
val currentItems = manager.childCount
val totaltItems = manager.itemCount
val scrolledItems = manager.findFirstVisibleItemPosition()
if (isScrolling && totaltItems == currentItems + scrolledItems && !isLoading && viewModel.searchedVideos.size <= 1_000_000) {
viewModel.getNextSearchResults(searchQuery)
isScrolling = false
} else {
videosAdapter.differ.submitList(viewModel.searchedVideos.toList())
recyclerView.setPadding(0, 0, 0, 0)
Log.d("Videos", viewModel.searchedVideos.size.toString())
}
}
}
override fun onPause() {
super.onPause()
Log.d("searched", "Search Results Activity paused")
}
override fun onStop() {
super.onStop()
Log.d("searched", "Search Results Activity stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("searched", "Search Results Activity destroyed")
}
}
The logcat shows the following error:
2021-09-28 12:51:06.200 1207-2988/? W/ActivityTaskManager: Force finishing activity com.example.mytube/.UI.SearchResultsActivity
For recreating the error, here is the gitHub repo to the app: https://github.com/AkhilrajNambiar/MyTube-1
Thanks in advance!
Currently i am working in Kotlin android development. while writing the MVVM architecture, i am stuck with ViewModel Creation
Error : Only classes are allowed on the left hand side of a class literal
Related classes are
Detail
BaseViewModel class, while creating the object of this class, we getting the error
package com.logicipher.mvvm.ui.base
import androidx.databinding.ObservableBoolean
import androidx.lifecycle.ViewModel
import com.logicipher.mvvm.data.DataManager
import com.logicipher.mvvm.utils.rx.SchedulerProvider
import io.reactivex.disposables.CompositeDisposable
import java.lang.ref.WeakReference
/**
* Created by Shamji N S on 20-08-2020.
*/
abstract class BaseViewModel<N>(
dataManager: DataManager,
schedulerProvider: SchedulerProvider
) : ViewModel() {
private val mDataManager: DataManager
private val mIsLoading = ObservableBoolean()
private val mSchedulerProvider: SchedulerProvider
private val mCompositeDisposable: CompositeDisposable
private var mNavigator: WeakReference<N>? = null
override fun onCleared() {
mCompositeDisposable.dispose()
super.onCleared()
}
fun getCompositeDisposable(): CompositeDisposable {
return mCompositeDisposable
}
fun getDataManager(): DataManager {
return mDataManager
}
fun getIsLoading(): ObservableBoolean {
return mIsLoading
}
fun setIsLoading(isLoading: Boolean) {
mIsLoading.set(isLoading)
}
fun getNavigator(): N? {
return mNavigator!!.get()
}
fun setNavigator(navigator: N) {
mNavigator = WeakReference(navigator)
}
fun getSchedulerProvider(): SchedulerProvider {
return mSchedulerProvider
}
init {
mDataManager = dataManager
mSchedulerProvider = schedulerProvider
mCompositeDisposable = CompositeDisposable()
}
}
BaseActivity
package com.logicipher.mvvm.ui.base
import android.annotation.TargetApi
import android.app.ProgressDialog
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.logicipher.mvvm.utils.CommonUtils
import com.logicipher.mvvm.utils.NetworkUtils
import dagger.android.AndroidInjection
/**
* Created by Shamji N S on 20-08-2020.
*/
abstract class BaseActivity<T : ViewDataBinding?, V : BaseViewModel<*>?> :
AppCompatActivity(), BaseFragment.Callback {
// TODO
// this can probably depend on isLoading variable of BaseViewModel,
// since its going to be common for all the activities
private var mProgressDialog: ProgressDialog? = null
private var mViewDataBinding: T? = null
private var mViewModel: V? = null
/**
* Override for set binding variable
*
* #return variable id
*/
abstract fun getBindingVariable(): Int
/**
* #return layout resource id
*/
#LayoutRes
abstract fun getLayoutId(): Int
/**
* Override for set view model
*
* #return view model instance
*/
abstract fun getViewModel(): V
override fun onFragmentAttached() {
}
override fun onFragmentDetached(tag: String?) {
}
override fun onCreate(savedInstanceState: Bundle?) {
performDependencyInjection()
super.onCreate(savedInstanceState)
performDataBinding()
}
fun getViewDataBinding(): T? {
return mViewDataBinding
}
#TargetApi(Build.VERSION_CODES.M)
fun hasPermission(permission: String?): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
checkSelfPermission(permission!!) == PackageManager.PERMISSION_GRANTED
}
fun hideKeyboard() {
val view = this.currentFocus
if (view != null) {
val imm =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
}
}
fun hideLoading() {
if (mProgressDialog != null && mProgressDialog!!.isShowing) {
mProgressDialog!!.cancel()
}
}
fun isNetworkConnected(): Boolean {
return NetworkUtils.isNetworkConnected(applicationContext)
}
fun openActivityOnTokenExpire() {
}
fun performDependencyInjection() {
AndroidInjection.inject(this)
}
#TargetApi(Build.VERSION_CODES.M)
fun requestPermissionsSafely(
permissions: Array<String?>?,
requestCode: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissions!!, requestCode)
}
}
fun showLoading() {
hideLoading()
mProgressDialog = CommonUtils.showLoadingDialog(this)
}
private fun performDataBinding() {
mViewDataBinding = DataBindingUtil.setContentView<T>(this, getLayoutId())
mViewModel = if (mViewModel == null) getViewModel() else mViewModel
mViewDataBinding!!.setVariable(getBindingVariable(), mViewModel)
mViewDataBinding!!.executePendingBindings()
}
}
MainActivity
package com.logicipher.mvvm.ui.main
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.ViewModelProviders
import com.logicipher.mvvm.BR
import com.logicipher.mvvm.R
import com.logicipher.mvvm.ViewModelProviderFactory
import com.logicipher.mvvm.databinding.ActivityMainBinding
import com.logicipher.mvvm.ui.base.BaseActivity
import javax.inject.Inject
/**
* Created by Shamji N S on 21-08-2020.
*/
class MainActivity : BaseActivity<ActivityMainBinding?, MainViewModel<MainNavigator>?>(),
MainNavigator /*, HasSupportFragmentInjector*/ {
/* #Inject
DispatchingAndroidInjector<Fragment> fragmentDispatchingAndroidInjector;*/
#set:Inject
internal var factory: ViewModelProviderFactory? = null
var mViewModel: MainViewModel<MainNavigator>? = null
var mBinding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: ")
mBinding = getViewDataBinding()
mViewModel?.setNavigator(this)
}
override fun getBindingVariable(): Int {
return BR.viewModel
}
override fun getLayoutId(): Int {
return R.layout.activity_main
}
override fun getViewModel(): MainViewModel<MainNavigator>? {
mViewModel =
ViewModelProviders.of(this, factory).get<MainViewModel<MainNavigator>>(MainViewModel<MainNavigator>::class.java)
return mViewModel
}
/*
#Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return fragmentDispatchingAndroidInjector;
}
*/
companion object {
private const val TAG = "MainActivity"
}
}
ViewModelProviderFactory
package com.logicipher.mvvm
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider.NewInstanceFactory
import com.logicipher.mvvm.data.DataManager
import com.logicipher.mvvm.ui.main.MainNavigator
import com.logicipher.mvvm.ui.main.MainViewModel
import com.logicipher.mvvm.utils.rx.SchedulerProvider
import java.lang.IllegalArgumentException
import javax.inject.Inject
/**
* Created by Shamji N S on 25-08-2020.
*/
public class ViewModelProviderFactory #Inject constructor(
dataManager: DataManager,
schedulerProvider: SchedulerProvider
) : NewInstanceFactory() {
private val dataManager: DataManager
private val schedulerProvider: SchedulerProvider
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return super.create(modelClass)
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel<T>(dataManager, schedulerProvider) as T
}
throw IllegalArgumentException("Unknown view model" + modelClass.name)
}
init {
this.dataManager = dataManager
this.schedulerProvider = schedulerProvider
}
}
Your BaseViewModel is abstract, you cannot create an object or an instance of an abstract class.
After you create a ViewModelFactory the way to create (or obtain) an instance of the ViewModel is this:
val viewModel = ViewModelProvider(this, viewModelFactory).get(ViewModelName::class.java)
However, I would consider using Kotlin Property Delegate from here which can help reduce the code.
I've created a simple project to study Kotlin and Android architecture
https://github.com/AOreshin/shtatus
The screen consists of RecyclerView and three EditTexts.
Corresponding ViewModel is exposing 7 LiveData's:
Three LiveData corresponding to filters
Event to notify the user that no entries are found
Event to notify the user that no entries are present
Status of SwipeRefreshLayout
List of connections to show based on filter input
When user types text in filter ViewModel's LiveData gets notified about the changes and updates the data. I 've read that it's a bad practice to expose MutableLiveData to Activities/Fragments but they have to notify ViewModel about the changes somehow. When no entries are found based on the user's input Toast is shown.
The problem
When the user enters filter values that have no matches, Toast is shown. If the user then rotates the device Toast is shown again and again.
I've read these articles:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
https://proandroiddev.com/livedata-with-single-events-2395dea972a8
But I don't understand how I can apply these to my use case. I think the problem in how I perform the updates
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>().also {
it.value = connections.value
}
private val refreshLiveData = MutableLiveData(RefreshStatus.READY)
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
val nameLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<String>()
val actualStatusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(actualStatusLiveData) { update() }
}
}
fun getRefreshLiveData(): LiveData<RefreshStatus> = refreshLiveData
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
private fun update() {
if (connections.value.isNullOrEmpty()) {
emptyTableEvent.call()
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
noMatchesEvent.call()
}
}
}
update() gets triggered on screen rotation because of new subscription to mediatorConnection and MediatorLiveData.onActive() is called. And it's intented behavior
Android live data - observe always fires after config change
Code for showing toast
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getRefreshLiveData().observe(viewLifecycleOwner, Observer { status ->
when (status) {
ConnectionListViewModel.RefreshStatus.LOADING -> refreshLayout.isRefreshing = true
ConnectionListViewModel.RefreshStatus.READY -> refreshLayout.isRefreshing = false
else -> throwException(status.toString())
}
})
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(nameLiveData.value)
urlEt.setText(urlLiveData.value)
statusCodeEt.setText(actualStatusLiveData.value)
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { nameLiveData.value = it.toString() }
urlEt.addTextChangedListener { urlLiveData.value = it.toString() }
statusCodeEt.addTextChangedListener { actualStatusLiveData.value = it.toString() }
}
}
private fun throwException(status: String) {
throw IllegalStateException(getString(R.string.error_no_such_status) + status)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getRefreshLiveData().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
}
How I should address this issue?
After some head scratching I've decided to go with internal ViewModel statuses, this way logic in Activity/Fragment is kept to a minimum.
So now my ViewModel looks like this:
package com.github.aoreshin.shtatus.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.aoreshin.shtatus.events.SingleLiveEvent
import com.github.aoreshin.shtatus.room.Connection
import io.reactivex.FlowableSubscriber
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subscribers.DisposableSubscriber
import okhttp3.ResponseBody
import retrofit2.Response
import java.util.function.Predicate
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class ConnectionListViewModel #Inject constructor(
private val connectionRepository: ConnectionRepository
) : ViewModel() {
private var tableStatus = TableStatus.OK
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>()
private val stopRefreshingEvent = SingleLiveEvent<Void>()
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
private val nameLiveData = MutableLiveData<String>()
private val urlLiveData = MutableLiveData<String>()
private val statusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(statusLiveData) { update() }
}
}
fun getStopRefreshingEvent(): LiveData<Void> = stopRefreshingEvent
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
fun getName(): String? = nameLiveData.value
fun getUrl(): String? = urlLiveData.value
fun getStatus(): String? = statusLiveData.value
fun setName(name: String) { nameLiveData.value = name }
fun setUrl(url: String) { urlLiveData.value = url }
fun setStatus(status: String) { statusLiveData.value = status }
private fun update() {
if (connections.value != null) {
if (connections.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.EMPTY) {
emptyTableEvent.call()
tableStatus = TableStatus.EMPTY
}
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.NO_MATCHES) {
noMatchesEvent.call()
tableStatus = TableStatus.NO_MATCHES
}
} else {
tableStatus = TableStatus.OK
}
}
}
}
fun send() {
if (!connections.value.isNullOrEmpty()) {
val singles = connections.value?.map { connection ->
val id = connection.id
val description = connection.description
val url = connection.url
var message = ""
connectionRepository.sendRequest(url)
.doOnSuccess { message = it.code().toString() }
.doOnError { message = it.message!! }
.doFinally {
val result = Connection(id, description, url, message)
connectionRepository.insert(result)
}
}
Single.mergeDelayError(singles)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { stopRefreshingEvent.call() }
.subscribe(getSubscriber())
} else {
stopRefreshingEvent.call()
}
}
private fun getSubscriber() : FlowableSubscriber<Response<ResponseBody>> {
return object: DisposableSubscriber<Response<ResponseBody>>() {
override fun onComplete() { Log.d(TAG, "All requests sent") }
override fun onNext(t: Response<ResponseBody>?) { Log.d(TAG, "Request is done") }
override fun onError(t: Throwable?) { Log.d(TAG, t!!.message!!) }
}
}
private fun getPredicate(): Predicate<Connection> {
return Predicate { connection ->
connection.description.contains(nameLiveData.value.toString(), ignoreCase = true)
&& connection.url.contains(urlLiveData.value.toString(), ignoreCase = true)
&& connection.actualStatusCode.contains(
statusLiveData.value.toString(),
ignoreCase = true
)
}
}
private enum class TableStatus {
NO_MATCHES,
EMPTY,
OK
}
companion object {
private const val TAG = "ConnectionListViewModel"
}
}
And corresponding Fragment looks like this:
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState != null) {
refreshLayout.isRefreshing = savedInstanceState.getBoolean(REFRESHING, false)
}
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getStopRefreshingEvent().observe(viewLifecycleOwner, Observer { refreshLayout.isRefreshing = false })
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(getName())
urlEt.setText(getUrl())
statusCodeEt.setText(getStatus())
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { setName(it.toString()) }
urlEt.addTextChangedListener { setUrl(it.toString()) }
statusCodeEt.addTextChangedListener { setStatus(it.toString()) }
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(REFRESHING, refreshLayout.isRefreshing)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getStopRefreshingEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
companion object {
private const val REFRESHING = "isRefreshing"
}
}
Pros
No additional dependencies
Usage of widespread SingleLiveEvent
Pretty straightforward to implement
Cons
Conditional logic is quickly getting out of hand even in this simple case, surely needs refactoring. Not sure if this approach will work in real-life complex scenarios.
If there are cleaner and more concise approaches to solve this problem I will be happy to hear about them!
In your solution; you introduced TableStatus which is acting like flag and not needed.
If you really looking for a good Android Architecture; instead you could just do
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_connections)
and
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_matches)
NOTE:
viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED
is not a patch Google implemented this fix in support library as well.
And remove #Singleton from (why would you need it to be singleton)
#Singleton
class ConnectionListViewModel #Inject constructor(
PS:
From the top of my head looks like; you may also don't need SingleLiveEvent for you case.
(would love to talk more on this if you want I also just have started Kotlin + Clear & Scalable Android Architecture)