I want to use mvvm in Dialog but I don't know how to pass LifecycleOwner to observe
class CommonDialog(context: Context) : Dialog(context, R.style.AppMaskStatusTheme) {
private val viewBinding: DialogSportOrderBinding by lazy { DialogSportOrderBinding.inflate(LayoutInflater.from(context)) }
private val viewModel by lazy { ViewModelProvider(context as ViewModelStoreOwner)[SportOrderViewModel::class.java] }
init {
setContentView(viewBinding.root)
viewModel.sportOrderList.observe(***what to pass here?***, androidx.lifecycle.Observer {
})
}
}
I have tried context as LifecycleOwner, context as AppCompatActivity but all fail
please help me, thanks!
You can create your own LifecycleOwner like this
class MyLifecycleOwner() : LifecycleOwner {
private val mLifecycleRegistry: LifecycleRegistry by lazy { LifecycleRegistry(this) }
init {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
fun stop() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
fun start() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
override fun getLifecycle(): Lifecycle = mLifecycleRegistry
}
Then your Dialog class will be like this
class CommonDialog(context: Context) : Dialog(context, R.style.AppMaskStatusTheme) {
private val viewBinding: DialogSportOrderBinding by lazy { DialogSportOrderBinding.inflate(LayoutInflater.from(context)) }
private val viewModel by lazy { ViewModelProvider(context as ViewModelStoreOwner)[SportOrderViewModel::class.java] }
private val lifeCycleOwner: MyLifecycleOwner by lazy { MyLifecycleOwner() }
init {
setContentView(viewBinding.root)
viewModel.sportOrderList.observe(lifeCycleOwner, {
})
}
override fun onStart() {
super.onStart()
lifeCycleOwner.start()
}
override fun onStop() {
super.onStop()
lifeCycleOwner.stop()
}
}
Read more about Lifecycle-Aware Components at here
android.content.Context does not implement android.arch.lifecycle.LifecycleOwner.
You'd have to pass an instance of AppCompatActivity, which implements android.arch.lifecycle.LifecycleOwner (or any other class which does that).
or cast (AppCompatActivity) context, when context is an instanceof AppCompatActivity.
Related
I am trying to get a callback from one child fragment to parent fragment in kotlin and then later call a function in parent fragment to manage other layouts inside the child fragment.
I know how to get callback from a fragment to the activity and access the function in the fragment in the callback we cannot do that in Kotlin because of companion objects.
Another way is to pass context of fragment to child fragment and implement the interface using fragment's content in onAttach() but I cannot pass the context of parent fragment via constructor as I am using factory static methods.
Any help will be highly appreciated.
Child Fragment : DriverStatusFragment.kt
companion object {
#JvmStatic
fun newInstance() =
DriverStatusFragment().apply {
arguments = Bundle().apply {
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnCurrentOrderStatusChanged) {
orderStatusChangedCallBack = context
}
}
private fun handleUserOnlineStatus(isOnline: Boolean) {
CustomPreferences.setBooleanPreference(context!!, PrefEntity.IS_ONLINE, isOnline)
when (isOnline) {
true -> {
binding.idOnlineStatus.text = getString(R.string.online)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_green)
//testing:
orderStatusChangedCallBack?.orderStatusChanged(CONSTANTS.NEW_ORDER)
}
false -> {
binding.idOnlineStatus.text = getString(R.string.offline)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_gray)
}
}
}
Parent Fragment : HomeFragment.kt
class HomeFragment : Fragment(), OnMapReadyCallback, OnCurrentOrderStatusChanged {
companion object {
#JvmStatic
fun newInstance() =
HomeFragment().apply {
arguments = Bundle().apply {
}
}
}
fun handleOrderStatus(status: String) {
when (status) {
CONSTANTS.IDLE -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
CONSTANTS.NEW_ORDER -> {
replaceFragment(
NewOrderFragment.newInstance(false),
NewOrderFragment::class.java.simpleName
)
}
CONSTANTS.ORDER_ACCEPTED -> {
replaceFragment(
EnRouteFragment.newInstance(CONSTANTS.ORDER_ACCEPTED),
EnRouteFragment::class.java.simpleName
)
}
CONSTANTS.ARRIVED_AT_DROP_LOCATION -> {
replaceFragment(
OrderDeliveredFragment.newInstance(CONSTANTS.ARRIVED_AT_DROP_LOCATION),
OrderDeliveredFragment::class.java.simpleName
)
}
CONSTANTS.DELIVERED -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
this.context = context
}
**NOT RECEIVING CALLBACK HERE**
override fun orderStatusChanged(orderStatus: String) {
CommonUtils.showToast(context!!, "REAched here")
handleOrderStatus(orderStatus)
}
}
I am trying to execute a method(fun onCreateViewModel()) in viewModel whenever the onCreate of the Activity is invoked. But the method is not getting executed.
ViewModel
class MainActivityViewModel(startingCount : Int) : ViewModel(), LifecycleObserver {
var count = 0
init {
count = startingCount
}
fun getCurrentCount(): Int{
return count
}
fun getUpdatedCount(): Int {
count ++
return count
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public fun onCreateViewModel(){
Log.i("Jts"," ViewModel created")
}
}
Activity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var user = User("Jeffin T", "26")
private lateinit var mainActivityViewModel: MainActivityViewModel
private lateinit var mainActivityViewModelFactory: MainActivityViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// mainActivityViewModel = ViewModelProviders.of()
mainActivityViewModelFactory = MainActivityViewModelFactory(123)
mainActivityViewModel = ViewModelProviders.of(this, mainActivityViewModelFactory)
.get(MainActivityViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.tvName.text = mainActivityViewModel.getCurrentCount().toString()
binding.user = user
binding.btnName.setOnClickListener {
binding.tvName.text = mainActivityViewModel.getUpdatedCount().toString()
}
}
private fun displayGreeting() {
binding.apply { tvName.setText("Hello! " + user?.name) }
}
}
You have to add the viewmodel as an observer of the lifecycle of your activity. You need to add this in your Activity
getLifecycle().addObserver(viewModel)
More info here
I have following project in Github : https://github.com/AliRezaeiii/TMDb-Paging
I have to postDelay calling methods in my ViewModel since datasource is not initialized :
abstract class DetailViewModel(private val item: TmdbItem) : BaseViewModel() {
private val handler = Handler(Looper.getMainLooper())
val trailers: ObservableList<Video> = ObservableArrayList()
val isTrailersVisible = ObservableBoolean(false)
private val _cast = MutableLiveData<List<Cast>>()
val cast: LiveData<List<Cast>> = _cast
val isCastVisible = ObservableBoolean(false)
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
protected abstract fun getTrailers(id: Int): Observable<List<Video>>
protected abstract fun getCast(id: Int): Observable<List<Cast>>
private fun showTrailers() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getTrailers(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ videos ->
if (videos.isNotEmpty()) {
isTrailersVisible.set(true)
}
with(trailers) {
clear()
addAll(videos)
}
}
) { throwable -> Timber.e(throwable) })
}
private fun showCast() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getCast(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ cast ->
if (cast.isNotEmpty()) {
isCastVisible.set(true)
}
this._cast.postValue(cast)
}
) { throwable -> Timber.e(throwable) })
}
}
And here is my Fragment :
abstract class DetailFragment<T : TmdbItem>
: BaseDaggerFragment(), CastClickCallback {
protected abstract fun getViewModel(): DetailViewModel
protected abstract fun getLayoutId(): Int
protected abstract fun initViewBinding(root: View): ViewDataBinding
protected abstract fun getTmdbItem(): T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val viewModel = getViewModel()
val root = inflater.inflate(getLayoutId(), container, false)
initViewBinding(root).apply {
setVariable(BR.vm, viewModel)
lifecycleOwner = viewLifecycleOwner
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(details_toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
summary_label.visibleGone(getTmdbItem().overview.trim().isNotEmpty())
// Make the MotionLayout draw behind the status bar
details_motion.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
summary.setOnClickListener {
val maxLine = resources.getInteger(R.integer.max_lines)
summary.maxLines = if (summary.maxLines > maxLine) maxLine else Int.MAX_VALUE
}
viewModel.cast.observe(viewLifecycleOwner, Observer {
it?.apply {
val adapter = CastAdapter(it, this#DetailFragment)
cast_list.apply {
setHasFixedSize(true)
cast_list.adapter = adapter
}
}
})
with(details_rv) {
postDelayed({ scrollTo(0, 0) }, 100)
}
}
return root
}
}
And BaseDaggerFragment :
open class BaseDaggerFragment : DaggerFragment() {
#Inject
lateinit var dataSource: RemoteDataSource
}
Could be any better solution than :
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
You can lazy initialize like this way
private val users:MutableLiveData<List<Cast>> by lazy {
MutableLiveData().also {
showTrailers()
showCast()
}
}
more details refer ViewModel
I need to edit a editText which is on main_layout from a fragment instead of MainActivity().
I've tried inflating main_layout to fragment but that doesn't worked (editText doesn't change), then I've tried to create method
fun changeEditText(){
editText.setText(R.string.name)
}
but when I call it in my Fragment using
MainActivity().changeEditText()
it gives me this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.ApplicationInfo android.content.Context.getApplicationInfo()' on a null object reference
How can I edit this editText from fragment?
You can't initialize an activity via its constructors. You can call the method from fragment like below
((MainActivity)getActivity()).changeEditText();
You can call getActivity() from the Fragment to get a reference to its parent activity:
(activity as? MainActivity)?.changeEditText()
However, a better approach would be to use a listener so the fragment doesn't care what it's parent activity has implemented:
interface Listener {
fun onTextChanged()
}
fun changeEditText() {
editText.setText(R.string.name)
listener?.onTextChanged()
}
class MainActivity : AppCompatActivity() {
//override ......
fun changeText() {
EditText editText = findViewById(R.id.my_edittext)
edittext.text = "something"
}
}
class MyFragment : Fragment() {
private lateinit var hostActivity: AppCompatActivity
override fun onAttach(context: Context){
hostActivity = context as AppCompatActivity
}
override fun onViewCreated(){
hostActivity.changeText()
}
}
Or
class MainActivity :AppCompatActivity(), MyCallback() {
// override ......
override fun onTextChange(){
val editText = findViewById(R.id.my_edittext)
edittext.text = "something"
}
}
class MyFragment : Fragment() {
private lateinit var myCallback: MyCallback
override fun onAttach(context: Context){
myCallback = context as MyCallback
}
override fun onViewCreated(){
myCallback.onTextChange()
}
}
interface MyCallback {
fun onTextChange()
}
Or
class MainActivity :AppCompatActivity(), MyCallback() {
override fun onCreate(savedInstanceState: Bundle?){
val sharedViewmodel = ViewmodelProviders.of(this).get(SharedViewModel.class)
sharedViewmodel.text.observe(this, object: Observer<String> {
override fun onChanged(text: String?){
val editText = findViewById(R.id.my_edittext)
edittext.text = text
}
})
}
}
class MyFragment : Fragment() {
private lateinit var hostActivity: AppCompatActivity
override fun onAttach(context: Context){
hostActivity = context as AppCompatActivity
}
override fun onViewCreated(){
val sharedViewmodel = ViewmodelProviders.of(hostActivity).get(SharedViewModel.class)
sharedViewmodel.text = "My new text"
}
}
class sharedViewModel: ViewModel(){
private val textHolderLiveData = MutableLiveData<String>()
fun getText(): LiveData<String> {
return textHolderLiveData
}
fun setText(text: String) {
textHolderLiveData.value = text
}
}
Hi I have LoginActivity and LoginViewModel and some more classes. I have showLoading and hideLoading in the BaseActivity so it can be accessible from each activity.
I am able to call LoginActivity method from the LoginViewModel like mNavigator?.startForgotPasswordActivity()
I want to call it from the LoginViewModel then what the way to do it using MVVM ? or I am going with wrong approach. Please suggest what is the correct way to do this ?
BaseActivity.kt
abstract class BaseActivity : AppCompatActivity(), AnkoLogger {
private val progressBar: ProgressBar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
protected fun getToolbar(): Toolbar {
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
return toolbar
}
protected 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)
}
}
#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 isNetworkConnected(): Boolean {
return NetworkUtils.isNetworkConnected(applicationContext)
}
fun showLoading() {
hideLoading()
// show progress bar
}
fun hideLoading() {
// hide progress bar
}
}
LoginActivity.kt
class LoginActivity : BaseActivity(), LoginNavigator {
#Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
performDependencyInjection()
super.onCreate(savedInstanceState)
val activityLoginBinding: ActivityLoginBinding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
activityLoginBinding.loginViewModel = loginViewModel
loginViewModel.mNavigator = this
}
override fun startHomeActivity() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun startRegistrationActivity() {
startActivity(Intent(this, RegistrationActivity::class.java))
}
override fun startForgotPasswordActivity() {
startActivity(Intent(this, ForgotPasswordActivity::class.java))
}
override fun handleError(throwable: Throwable) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
LoginViewModel.kt
class LoginViewModel : BaseViewModel<LoginNavigator>(), AnkoLogger {
val emailField = ObservableField<String>()
private val email: String
get() = emailField.get()
val passwordField = ObservableField<String>()
private val password: String
get() = passwordField.get()
#Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
fun login(view: View) {
if (isEmailAndPasswordValid(email, password)) {
ApiHelperImpl().doServerLoginApiCall(email, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : CallbackWrapper<LoginResponse>() {
override fun onSuccess(loginResponse: LoginResponse) {
info { loginResponse }
}
})
}
}
/**
* Validate email and password. It checks email and password is empty or not
* and validate email address is correct or not
* #param email email address for login
* #param password password for login
* #return true if email and password pass all conditions else false
*/
private fun isEmailAndPasswordValid(email: String, password: String): Boolean {
if (email.isEmpty()) return false
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) return false
if (password.isEmpty()) return false
return true
}
}
BaseViewModel.kt
abstract class BaseViewModel<N> {
var mNavigator: N? = null
}
There can be 2 approach for same
1) Use all functions i.e related to UI update or UI event listener from a view (Activity or Fragment) according to mvp and from viewmodel only try to manage data like api's and other logic
class LoginActivity : BaseActivity(), LoginNavigator {
#Inject
lateinit var mLoginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
mLoginViewModel.mNavigator = this
callApi()
}
private fun callApi() {
showLoading()
mLoginViewModel.callApi()
}
override fun openHomeScreen(model: Model) {
hideLoading()
showSnackBar(constraint_root, model.Domain)
}
}
class LoginViewModel(sessionManager: SessionManager, requestInterface: RequestInterface) : BaseViewModel<LoginNavigator>(sessionManager, requestInterface) {
fun callApi() {
requestInterface.getServiceIP()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResponse, this::handleError)
}
private fun handleResponse(model: Model) {
if (model.Domain == null) {
mNavigator!!.openHomeScreen(model)
} else {
}
}
private fun handleError(error: Throwable) {
error.printStackTrace()
}
}
2)
In Login interface add a function
interface LoginNavigator {
fun openHomeScreen()
fun getActivity(): BaseActivity
}
In LoginActivity override the function and return
override fun getActivity(): BaseActivity = this
Now using navigator you can access base activity & call show/hide loader function
mNavigator!!.getActivity().showLoading()