Итак, я пытаюсь сделать следующее в своем приложении android:
- Получить JSON из REST API, проанализировать его и вложенные списки
- Store значения, полученные из этого JSON в локальной базе данных ROOM
- Получить значения из базы данных и отобразить их на моем фрагменте с использованием архитектуры MVVM
# 1 был успешным. Я могу использовать Retrofit2 для извлечения, анализа и отображения данных
# 2 Я не уверен. ROOM явно недружелюбен к сложным типам данных. Я продолжал получать ошибку cannot find setter method
для каждого val
, который я пытался создать, несмотря на использование конструктора, инициализацию значений и установку @Ignore для списков и объектов. Итак, все val
в классе данных были преобразованы в var
, и проект, наконец, построен. Но я не знаю, вызовет ли это огромные проблемы. Я не знаю, была ли база данных успешно создана, но она генерирует точную схему (так что да?)
# 3, я думаю, что реализовал ее, следуя руководству, но теперь созданный мной метод, который связывает пользовательский интерфейс и загружает RecyclerView, ничего не возвращает. В остальном программа выполняется без ошибок или исключений.
Я думаю, что проблема может быть в разделе FRAGMENT при выполнении bindsui
внутри onCreateView
. Моя IDE дает подсказку о том, что возвращаемое значение не используется, и я не знаю, почему
Я также не знаю, напутал ли я что-то, потому что я вижу в журнале Background young concurrent copying GC freed 2775(325KB) AllocSpace objects, 0(0B) LOS objects, 15% free, 1988KB/2355KB, paused 731us total 437.316ms
, который очевидно означает, что мое приложение забирает много памяти (хотя я даже не загружаю и не кэширую столько данных).
См. ниже:
DATA CLASS / ENTITY
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
const val RESTAURANT_ID = 0
@Entity(tableName = "nearby_restaurant")
data class NearbyRestaurant(
@SerializedName("nickname")
var nickname: String,
@SerializedName("address")
var address: String,
@SerializedName("app")
var app: String,
@SerializedName("catchUpClose")
@field:Embedded(prefix = "catchUpClose_")
var catchUpClose: CatchUpClose?,
@SerializedName("cateringAdvanceTime")
var cateringAdvanceTime: Int,
@SerializedName("city")
var city: String,
@SerializedName("createdAt")
var createdAt: String,
@SerializedName("curbside")
var curbside: Boolean,
@SerializedName("delivery")
var delivery: Boolean,
@SerializedName("deliveryByTap")
var deliveryByTap: Boolean,
@SerializedName("deliveryFee")
var deliveryFee: Int,
@SerializedName("deliveryRadius")
var deliveryRadius: Int,
@SerializedName("description")
var description: String,
@SerializedName("dinein")
var dinein: Boolean,
@SerializedName("distanceInMeters")
var distanceInMeters: Double,
@SerializedName("dollars")
var dollars: Int,
@SerializedName("flags")
@field:Embedded(prefix = "flags_")
var flags: Flags?,
@SerializedName("holidaySchedule")
@Ignore
var holidaySchedule: List<String>?,
@SerializedName("isNew")
var Newis: Boolean,
@SerializedName("latitude")
var latitude: Double,
@SerializedName("logo")
var logo: String,
@SerializedName("longitude")
var longitude: Double,
@SerializedName("minimumDelivery")
var minimumDelivery: Int,
@SerializedName("name")
var name: String,
@SerializedName("offline")
var offline: Boolean,
@SerializedName("phone")
var phone: String,
@SerializedName("flags")
@field:Embedded(prefix = "schedule_")
var schedule: Schedule?,
@SerializedName("state")
var state: String,
@SerializedName("stopOrders")
var stopOrders: Int,
@SerializedName("takeout")
var takeout: Boolean,
@SerializedName("updatedAt")
var updatedAt: String,
@SerializedName("vacation")
@Ignore
var vacation: List<String>?,
@SerializedName("working")
@Ignore
var working: List<Int>?,
@SerializedName("zip")
var zip: String
) {
@field:PrimaryKey(autoGenerate = false)
var restaurant_id: Int = RESTAURANT_ID
@JvmOverloads
constructor() : this("", "", "", null, 0, "", "", false, false, false,
0, 0, "", false, 0.0, 0, null, null, false, 0.0, "", 0.0, 0,
"", false, "", null,
"", 0, false, "", null, null, "" )
}
DAO
@Dao
interface RestaurantsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(restaurantEntry: List<NearbyRestaurant>)
@Query("SELECT * FROM nearby_restaurant WHERE restaurant_id = $RESTAURANT_ID")
fun getNearbyDB(): LiveData<NearbyRestaurant>
}
БАЗА ДАННЫХ
@Database(
entities = [NearbyRestaurant::class],
version = 2
)
abstract class RestaurantsDB : RoomDatabase() {
abstract fun restaurantsDao(): RestaurantsDao
companion object {
@Volatile private var instance: RestaurantsDB? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDB(context).also { instance = it}
}
private fun buildDB(context: Context) =
Room.databaseBuilder(context.applicationContext,
RestaurantsDB::class.java, "restaurants.db")
.build()
}
}
ФРАГМЕНТ
// custom scope
class RestaurantsFragment : ScopedFragment(), DIAware {
override val di: DI by closestDI()
private val viewModelFactory: RestaurantsViewModelFactory by instance1()
private lateinit var viewModel: RestaurantsViewModel
val TAG = "APP-DEBUG"
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.restaurants_fragment, container, false)
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, viewModelFactory)
.get(RestaurantsViewModel::class.java)
Log.e(TAG, "about to bind ui ")
bindUI()
}
fun load_recycler_view(nearbyResponses: List<NearbyRestaurant>) {
Log.e(TAG, "recyclerview invoked")
recycle_restaurants.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = RestaurantAdapter(nearbyResponses)
setHasFixedSize(true)
}
}
private fun bindUI(): () -> Job = {launch {
val nearbyrestaurant = viewModel.nearby.await()
nearbyrestaurant.observe(viewLifecycleOwner, Observer {
if (it == null) return@Observer
load_recycler_view(listOf(it))
})
}
}
}
ПРОСМОТР МОДЕЛИ
class RestaurantsViewModel(private val restaurantsRepository: RestaurantsRepository) : ViewModel() {
val nearby by lazyDeferred { restaurantsRepository.getNearbyRestaurants() }
}
---------------------------------------------------------------------------------------------
// lazyDeferred (custom)
fun <T> lazyDeferred(block: suspend CoroutineScope.() -> T): Lazy<Deferred<T>> {
return lazy {
GlobalScope.async(start = CoroutineStart.LAZY) {
block.invoke(this)
}
}
}
ПРОСМОТР МОДЕЛИ
class RestaurantsViewModelFactory(
private val restaurantsRepository: RestaurantsRepository
) : ViewModelProvider.NewInstanceFactory(){
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return RestaurantsViewModel(restaurantsRepository) as T
}
}
РЕПОЗИТОРИЙ
class RestaurantsRepositoryImpl(
private val restaurantsDao: RestaurantsDao,
private val restaurantNetworkDataSource: RestaurantNetworkDataSource
) : RestaurantsRepository {
init {
restaurantNetworkDataSource.downloadedRestaurants.observeForever { newRestaurant ->
//persist
persistFetchedRestaurant(newRestaurant)
}
}
override suspend fun getNearbyRestaurants(): LiveData<NearbyRestaurant> {
return withContext(Dispatchers.IO) {
initRestaurantData()
return@withContext restaurantsDao.getNearbyDB()
}
}
private fun persistFetchedRestaurant(fetchedRestaurant: NearbyResponse) {
GlobalScope.launch(Dispatchers.IO) {
restaurantsDao.upsert(fetchedRestaurant.nearbyRestaurant)
}
}
private suspend fun initRestaurantData() {
if (isFetchRestaurantNeeded(ZonedDateTime.now().minusHours(1)))
fetchRestaurants()
}
private suspend fun fetchRestaurants() {
restaurantNetworkDataSource.fetchRestaurants(
3,
40.7539,
74.40816,
3
)
}
private fun isFetchRestaurantNeeded(lastFetchTime: ZonedDateTime): Boolean {
val thirtyMinutesAgo = ZonedDateTime.now()
return lastFetchTime.isBefore(thirtyMinutesAgo)
}
}
ЗАВИСИМОСТЬ ВПРЫСКА
import org.kodein.di.*
import org.kodein.di.android.x.androidXModule
class MyApp : Application(), DIAware {
override val di: DI
get() = DI.lazy {
import(androidXModule(this@MyApp))
bind() from singleton { RestaurantsDB(instance()) } // this is my database
bind() from singleton { instance<RestaurantsDB>().restaurantsDao() }
bind<ConnectivityInterceptor>() with singleton {ConnectivityInterceptorImpl(instance())} // ConnectivityInterceptor checks for network validity and prevents crashing if no network connection
bind() from singleton { tapAPI(instance()) } // this interface is the structure of my retrofit caller
bind<RestaurantNetworkDataSource>() with singleton { RestaurantNetworkDataSourceImpl(instance()) } // this fetches and downloads the data
bind<RestaurantsRepository>() with singleton { RestaurantsRepositoryImpl(instance(), instance()) } //connects to repository
bind() from provider { RestaurantsViewModelFactory(instance()) } //push to fragment
}
}