I am new at Kotlin and trying to implement MVP Architecture,
Currently I am having problem initializing/setting textview's value outside onCreate() method
here is my code
SplashActivity.kt
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
lateinit var appDetail: AppDetail
lateinit var textTitle: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textTitle = findViewById(R.id.splash_txt_title) as TextView
AppSingleton.appContext = applicationContext
var splashPresentation = SplashPresentation(this)
splashPresentation.getAppDetailFromService()
}
override fun fetchAppDetailSuccessful(response: SplashServiceObject) {
AppSingleton.initializeAppDetal(Gson().fromJson(response.json_string, AppDetail::class.java))
this.appDetail = AppSingleton.appDetail
}
override fun fetchAppDetailFailed(errMsg: String) {
textTitle.text = errMsg
}
}
SplashPresenter.kt
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
fun getAppDetailFromService() {
var splashService = SplashService()
splashService.getAppDetailFromAssets(this)
}
override fun fetchAppDetailFromServiceSuccessful(response: SplashServiceObject) {
view.fetchAppDetailSuccessful(response)
}
override fun fetchAppDetailFromServiceFailed(errMsg: String) {
view.fetchAppDetailFailed(errMsg)
}
}
SplashService.kt
class SplashService {
fun getAppDetailFromAssets(splashPresentation: SplashPresentation) {
val json_filename = "appdetail.json"
var jsonResponse: JsonResponse = AppSingleton.commonUtils.fetchJsonFromAssets(json_filename, AppSingleton.appContext!!)
if (jsonResponse.json_status) {
var splashServiceObj = SplashServiceObject
splashServiceObj.json_string = jsonResponse.json_info
splashServiceObj.response_msg = "JSON Successful fetched."
splashPresentation.fetchAppDetailFromServiceSuccessful(splashServiceObj)
} else {
splashPresentation.fetchAppDetailFromServiceFailed(jsonResponse.json_info)
}
}
}
in my SplashActivity().onCreate(), I am calling a Presenter that access Service, then the Service return a value to Presenter,
Then Presenter, return value to my SplashActivity's View, one of the function is, fetchAppDetailFailed(errMsg)
when I run the app, it crashes, saying the "textaa" is not yet initialized.
back in Java exp, when the variable is already instantiated on onCreate(), you can call this variable anywhere within the activity.
Thanks in advance!
You cannot instantiate Activities on Android. They are instantiated by the OS, and the OS calls the lifecycle methods on it.
In an MVP pattern, the View and Presenter both reference each other. Since Activity (the View) is the entry point of the application, your Activity should instantiate the Presenter and pass a reference of itself to the Presenter so communication can go both ways.
Also, the reference to the activity in the Presenter should be specified as a ViewInterface, not an Activity, or you're kind of defeating the purpose of using MVP.
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
//... methods that call functions on view
}
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
private val presenter = SplashPresentation(this)
//...
}
Related
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
I'm trying to DRY up my code and I have a couple activities which use the same blocks of code which I want to move into a method in the parent activity. The problem is that this code uses generated ViewBindings which are unique classes, and I can't figure out what the parent class is in order to use it as a method parameter.
For example, this code is in two different activities and the only difference is that in one activity binding = Activity1Binding, in the other one it's Activity2Binding. They share some views with the same IDs.
binding.noteTitleTV.setOnClickListener { changeTitle() }
binding.deleteModalLayout.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.cancelDeleteButton.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.confirmDeleteButton.setOnClickListener { onDeleteNoteClicked() }
I would like to implement something like this in the parent activity to prevent duplicate code, if that's possible:
fun setUp(binding: [BINDING PARENT CLASS]) {
binding.noteTitleTV.setOnClickListener { changeTitle() }
// etc
}
The generated classes extend the Object class (java.lang.Object)
The binding class inherits from ViewDataBinding, so you could do this (Kotlin code)
fun setUp(binding: ViewDataBinding) {
when(binding){
is Activity1Binding -> { (binding as Activity1Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
is Activity2Binding -> { (binding as Activity2Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
}
// etc
}
I don't know that it could get more "generic" than that as you don't control the generated classes. But that would at least allow you to place all the code in a single class as you suggested. I use a similar approach in that I have an lateinit instance of all of my generated binding classes and just set which is active based on the passed variable and use that instance name so i don't have to keep typing as.
ex:
private lateinit var itemBinding : GroceryItemBinding
private lateinit var maintItemBinding : GroceryItemMaintBinding
private lateinit var compareItemBinding : GroceryItemCompareBinding
private lateinit var priceItemBinding : GroceryItemPriceBinding
private lateinit var adItemBinding : GroceryItemAdBinding
when(viewBinding){
is GroceryItemMaintBinding -> {
maintItemBinding = viewBinding as GroceryItemMaintBinding
maintItemBinding.groceryItem = gi
maintItemBinding.groceryItemImage.setOnClickListener { ... }
......
}
is GroceryItemBinding -> {
itemBinding = viewBinding as GroceryItemBinding
}
......
}
ViewBinding can create by bind(view), so you can create a base class like this:
abstract class BaseActivity : AppCompatActivity() {
private lateinit var binding: Activity1Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = createContentView()
// create ViewBinding
binding = Activity1Binding.bind(view)
}
// create view by subclass
abstract fun createContentView(): View
fun setTextViewTitle(text: CharSequence) {
binding.tvTitle.text = text
}
}
this is the content of Activity1Binding#bind():
#NonNull
public static ActivityMainBinding bind(#NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.layout;
FinanceLabelLayout layout = rootView.findViewById(id);
if (layout == null) {
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, layout);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
But this is not recommended.
This is not type safe.
I have a ViewModel that inherits from a base class and I would like to have a corresponding Activity also inherit from a base class. The activity would call the same method of the derived ViewModel each time. So it would be something like this:
BaseViewModel:
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
protected val context = getApplication<Application>().applicationContext
protected var speechManager: SpeechRecognizerManager? = null
var _actionToTake : MutableLiveData<AnalyseVoiceResults.Actions> = MutableLiveData()
var actionToTake : LiveData<AnalyseVoiceResults.Actions> = _actionToTake
open fun stopListening() {
if (speechManager != null) {
speechManager?.destroy()
speechManager = null
}
open fun startListening() {
val isListening = speechManager?.ismIsListening() ?: false
if (speechManager == null) {
SetSpeechListener()
} else if (!isListening) {
speechManager?.destroy()
SetSpeechListener()
}
}
}
BaseActivity
class BaseActivity : AppCompatActivity() {
private lateinit var baseViewModel: BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun goback() {
super.onBackPressed()
baseViewModel.stopListening()
finish()
}
fun startListening() {
baseViewModel.startListening()
}
override fun onDestroy() {
super.onDestroy()
baseViewModel.stopListening()
}
}
Derived Activity:
class DerivedActivity : BaseActivity() {
private val nextActivityViewModel: NextActivityViewModel by inject()
///^^inherits from BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*** pass reference ***/
baseViewModel = nexActivityViewModel
nextActivityViewModel.actionToTake.observe(this, object : Observer<AnalyseVoiceResults.Actions?> {
override fun onChanged(t: AnalyseVoiceResults.Actions?) {
if (t?.equals(AnalyseVoiceResults.Actions.GO_BACK) ?: false) {
goback()
}
}
})
startListening()
}
}
Will this cause memory leaks to have two instances of a view model for this activity? Is there a better way to do this? I don't want to keep repeating the same code for all my activities. (I would also have the same question if I was doing this with one base fragment).
make this var baseViewModel: BaseViewModel an abstract variable where all the children class must override it. So, when you call the startListening and stopListening, these methods will be called from children implementation.
Edit:
Make the BaseActivity an abstract class and the baseViewModel as an abstract variable
abstract class BaseActivity : AppCompatActivity() {
private abstract var baseViewModel: BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun goback() {
super.onBackPressed()
baseViewModel.stopListening()
finish()
}
fun startListening() {
baseViewModel.startListening()
}
override fun onDestroy() {
super.onDestroy()
baseViewModel.stopListening()
}
}
So, your DerivedActivity must override the baseViewModel, and every call on father's class will trigger the child
class DerivedActivity : BaseActivity() {
override val baseViewModel: NextActivityViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Will this cause memory leaks to have two instances of a view model for
this activity?
No, there are no memory leaks with this approach. Nor do you have 2 instances of the ViewModel for the same activity. It's a single instance of ViewModel which is being referenced with different variables in BaseActivity and BaseViewModel.
Is there a better way to do this?
The first issue I see is that you have Android specific code in the ViewModels, which is not considered a good practice. You should move the speech manager code to the base activity itself, and ViewModel should only hold the "state" data that you want to retain on orientation changes. This will ensure all the Speech Management methods (create, resume, destroy) will be in the base activity. Concrete activity will only have observers if the state changes.
If you are following any architecture pattern (like MVP), once you move the Speech Manager code out to activity, it would become obvious to move this further out to the Presenter.
EDIT: I have not used the MVVM pattern in production, but this is a light variant of what you may want:
The basic idea is to move Speech management code in a lifecycle-aware component. All UI code in view/activity and business logic / non-android state in viewmodel. I don't see a point in having base activity or viewmodel based on the requirements you have shared so far.
/**
* All the speech related code is encapsulated here, so any new activity/fragment can use it by registering it's lifecycle
*/
class SpeechManager(private val context: Context): LifecycleObserver {
val TAG = "SpeechManager"
private var speechRecognizer: SpeechRecognizer? = null
fun registerWithLifecycle(lifecycle: Lifecycle) {
Log.e(TAG, "registerWithLifecycle")
lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
Log.e(TAG, "start")
speechRecognizer = (speechRecognizer ?: SpeechRecognizer.createSpeechRecognizer(context)).apply {
// setRecognitionListener(object : RecognitionListener {
// //implement methods
// })
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
Log.e(TAG, "stop")
speechRecognizer?.run {
stopListening()
destroy()
}
}
}
ViewModel:
class SpeechViewModel: ViewModel() {
val TAG = "SpeechViewModel"
//List all your "data/state" that needs to be restores across activity restarts
private val actions: MutableLiveData<Actions> = MutableLiveData<Actions>().apply { value = Actions.ActionA }
//Public API for getting observables and all use-cases
fun getActions() = actions
fun doActionA(){
//validations, biz logic
Log.e(TAG, "doActionA")
actions.value = Actions.ActionA
}
fun doActionB(){
Log.e(TAG, "doActionB")
actions.value = Actions.ActionB
}
}
sealed class Actions{
object ActionA: Actions()
object ActionB: Actions()
}
Activity/View:
class SpeechActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_speech)
setSupportActionBar(toolbar)
initSpeechManager()
}
private lateinit var speechManager: SpeechManager
private lateinit var speechViewModel: SpeechViewModel
/**
* Register lifecycle aware components and start observing state changes from ViewModel.
* All UI related code should ideally be here (or your view equivalent in MVVM)
*/
private fun initSpeechManager() {
speechManager = SpeechManager(this).apply {registerWithLifecycle(lifecycle)}
speechViewModel = ViewModelProviders.of(this).get(SpeechViewModel::class.java).apply {
getActions().observe(this#SpeechActivity, Observer<Actions>{
when(it){
is Actions.ActionA -> {
Log.e(TAG, "Perform ActionA")
speechManager.start()
}
is Actions.ActionB -> {
Log.e(TAG, "Perform ActionB")
speechManager.stop()
super.onBackPressed()
}
}
})
}
}
}
I have an Activity that has a fragment with a viewmodel in it. The Activity needs to be able to update the livedata object's value as well as does the fragment.
I declared my ViewModel for the fragment like this:
class BottomNavViewModel:ViewModel() {
var isConnected = MutableLiveData<Boolean>()
}
In the BottomNavFragment I have this code to declare the ViewModel
private val viewModel: BottomNavViewModel by lazy { ViewModelProviders.of(this).get(BottomNavViewModel::class.java) }
A few lines below that I have this:
private val changeObserver = Observer<Boolean> { value ->
value?.let {
Timber.i("Update of isConnected received. Updating text field now")
if(it) {
connectedText.text = getString(R.string.connected)
connectedText.setTextColor(activity!!.getColor(R.color.colorSelectedGreen))
}
else {
connectedText.text = getString(R.string.not_connected)
connectedText.setTextColor(activity!!.getColor(R.color.off_red))
}
}
...
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is BottomNavFragment.OnFragmentInteractionListener) {
listener = context
}
else {
throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
}
viewModel.isConnected.observe(this, changeObserver)
}
That Observer never, ever, gets hit.
In my Activity I have this:
private var sharedBottomNavViewModel:BottomNavViewModel? = null
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connection)
sharedBottomNavViewModel = ViewModelProviders.of(this).get(BottomNavViewModel::class.java)
...
override fun onResume() {
super.onResume()
startBackgroundThread()
checkCameraPermission()
//TODO: Change this to listen for a connection
sharedBottomNavViewModel?.let {
Timber.i("Updating isConnected to true now")
it.isConnected.value = true
}
}
In the logs I see the messages indicating that the update occurs but the observer never gets the message.
Can anyone tell me what I am doing wrong here please?
Your 2 viewmodels are not the same. You are creating a viewmodel and passing the lifecycle owner, and in one case you specify the fragment, and in the other the activity.
Change your fragment like this:
private val viewModel: BottomNavViewModel by lazy { ViewModelProviders.of(activity).get(BottomNavViewModel::class.java) }
Be careful where you initialize the viewmodel though, as activity (getActivity()) is nullable.
Edit: (credit Ian Lake) Or, if you use the fragment-ktx artifacts, you can do this
private val viewModel: BottomNavViewModel by activityViewModels()
I have this function in kotlin extension file to pass method but it doesn't work. Please explain me how it make correctly, I try this:
fun showErrorClientScreen(context: Context, action : () -> Unit) {
val intent = Intent(context, RestClientErrorActivity::class.java)
val bundle = Bundle()
bundle.putSerializable(UPDATE_CLIENT_ERROR, ErrorClientListener { action })
intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)
context.startActivity(intent)
}
use java interface
public interface ErrorClientListener extends Serializable {
void tryAgainFunction();
}
and my activity where i need listen click button and try again send request:
class RestClientErrorActivity: BaseActivity(), View.OnClickListener {
private lateinit var errorClientListener: ErrorClientListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rest_client_error)
try {
val bundle = intent.getBundleExtra(UPDATE_CLIENT_ERROR_BUNDLE)
errorClientListener = bundle?.getSerializable(UPDATE_CLIENT_ERROR) as ErrorClientListener
} catch (e: Exception) {
e.message
}
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.ib_update -> errorClientListener.tryAgainFunction()
}
}
}
It is quite strange to package interfaces between activities and it is definitely not advisable. One reason why it is maybe not serializing between Activity A and Activity B is because the object was created in Activity A, it is treated as anonymous class creation and Activity A holds the reference to this object, hence preventing it from being serialised. This is good, because you can create references to objects within the interface callback whose reference in turn would be held by class instantiating it. Therefore, garbage collector won't be able to run collections on these objects and free up the space; causing a massive memory leak.
The alternative approach to your problem could be using clean architectures and a Singleton class pattern that is accessible by both activities and instantiated only once by say Activity A:
class SingletonErrorHandler private constructor(){
var isError = false
fun doOnError() {
// do non view related stuff
// like a network call or something
}
companion object {
val instance by lazy { SingletonErrorHandler() }
}
}
in the activity you can define
class ActivityA : AppCompatActivity() {
fun onError() {
SingletonErrorHandler.instance.isError = true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.a_activity)
}
}
in activity B
class ActivityB : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.b_activity)
val errorHandler = SingletonErrorHandler.instance
if(errorHandler.isError)
errorHandler.doOnError()
}
}
You can write factory method to start the activity like android studio generates factory method for fragment creation.
class RestClientErrorActivity : AppCompatActivity() {
companion object {
private var completion: (() -> Unit)? = null
fun start(context: Context, completion: (() -> Unit)?) {
RestClientErrorActivity.completion = completion
val bundle = Bundle()
intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)
context.startActivity(intent)
}
}
private lateinit var retryButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retryButton = findViewById(R.id.btn_retry)
}
fun onRetryClick(view: View) {
finish()
completion?.invoke()
}
}
Note: completion is not mandatory. so i made that as nullable. if you start activity without using factory method app will not crash.
I had the same problem. As mentioned in HawkPriest's Answer, your object is not serializable, because its an anonymous class. Another way to fix this is to simply implement a non-anonymous class that implements your interface. Here is my code:
Interface
interface MyInterface : Serializable {
fun instruction()
}
Class
class MyClass : MyInterface {
override fun instruction() {
// does something
}
}
Calling Activity
val myObject = MyClass()
val intent = Intent(context, MyActivity::class.java).putExtra("Tag", myObject)
context.startActivity(intent)
Activity
override fun onCreate(savedInstanceState: Bundle?) {
val myObject = intent.getSerializableExtra("Tag") as MyInterface
myObject.instruction()
}
Regarding the "native resources" as mentioned in your comment, you can make your instruction take parameters or pass them to your MyObject.
P.S. The problems I have with the Singleton solution:
Singleton is not eligable for garbage collection, which means it lives on after its not needed anymore. (not 100% sure about that, but that's what I get from this answer)
Using singleton would mean you cant have "multiple different uses" for your activity. If an interface is used, it is to be able to use multiple different implementations of that interface. A singleton wouldn't provide that, without using an interface architecture within your singleton, which would then again render it unnecessary, considering my proposed solution.