Ders 12 / 20
Bildirimler ve Derin Bağlantı
Bildirime dokunmak ya da bir bağlantıyı açmak uygulamayı belirli bir ekranda başlatır. Ders açık uygulamada bağlantının yığına tek bir ekran kurduğunu, kapalı uygulamada ise ağacın tamamının kurulduğunu ölçer.
Yayın Güncelleme
Bu derste
- Derin bağlantının rota listesine nasıl çevrildiğini açıklar
- Uygulama açıkken bağlantının yığına yalnız hedef ekranı kurduğunu ölçer
- Uygulama kapalıyken bağlantının ağacın tamamını kurduğunu ölçer
- Tanınmayan ve yinelenen bağlantının ağaçta komut doğurmadığını gösterir
İçindekiler
Önceki derslerde ağaçtaki değişiklik uygulamanın içinden geldi: bir dokunuş, bir yanıt, bir oturum. Bir değişiklik de dışarıdan gelir. Kullanıcı bildirime dokunur ya da bir bağlantıya tıklar; uygulama, sipariş 42’nin ekranında açılmak zorundadır. Bu ders dışarıdan gelen adresin rota listesine nasıl çevrildiğini ve uygulamanın açık ya da kapalı olmasının ağaçta ne fark ettiğini ele alır.
Kursun sorusu değişmez: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Akıbet korunan, güncellenen, kurulan ya da sökülendir; güncellenen, kurulan ve sökülen düğümlerin toplamı komuttur. Akıbeti elle atamayız, kural verir:
// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"]; export function el(type, props = {}, children = [], key = null) { return { type, props, children, key }; } function segment(node) { return node.key === null ? node.type : `${node.type}[${node.key}]`; } function identity(node, index) { return node.key === null ? `#${index}` : `key:${node.key}`; } function sameProps(a, b) { const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort(); return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n])); } function whole(node, path, fate, ops) { const here = `${path}/${segment(node)}`; ops.push({ fate, path: here }); node.children.forEach((child) => whole(child, here, fate, ops)); } export function reconcile(prev, next, path = "", ops = []) { if (prev === null) { whole(next, path, "kurulan", ops); return ops; } if (next === null) { whole(prev, path, "sokulen", ops); return ops; } if (prev.type !== next.type || prev.key !== next.key) { whole(prev, path, "sokulen", ops); whole(next, path, "kurulan", ops); return ops; } const here = `${path}/${segment(next)}`; ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here }); const before = new Map(prev.children.map((child, i) => [identity(child, i), child])); const matched = new Set(); next.children.forEach((child, i) => { const id = identity(child, i); matched.add(id); reconcile(before.has(id) ? before.get(id) : null, child, here, ops); }); prev.children.forEach((child, i) => { if (!matched.has(identity(child, i))) reconcile(child, null, here, ops); }); return ops; } export function printOps(ops) { ops.forEach((op) => console.log(`${op.fate}: ${op.path}`)); } export function countFates(ops) { for (const name of FATES) { console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`); } console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`); }
Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Kural bağlantının nereden geldiğini görmez, yalnız önceki ve sonraki ağacı karşılaştırır.
Bağlantı Rotaya Dönüşür
Derin bağlantı, uygulamanın belirli bir ekranını gösteren bir adrestir. Bildirim bu adresi taşır, bir e-postadaki bağlantı ya da bir başka uygulama onu açar. Bağlantı ekran değildir: adres bir metindir ve uygulamanın rota tablosu onu bir rotaya çevirir. Platform bağlantıyı iki yoldan iletir. Uygulama açıksa bağlantı çalışan uygulamaya bir olay olarak gelir. Uygulama kapalıysa bağlantı, uygulamayı başlatan ilk adres olarak alınır.
// Platform kodu: NotificationTap
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let link = response.notification.request.content.userInfo["link"] as? String
pendingLink = link
completionHandler()
}
Platform tarafı bağlantıyı yakalar ve JavaScript tarafına iletir. Çerçevede iki giriş vardır: açılışta ilk adresi sormak ve açıkken gelen olayı dinlemek.
// Platform kodu: LinkListener
function useLink(onLink) {
useEffect(() => {
Linking.getInitialURL().then((url) => url && onLink(url));
const sub = Linking.addEventListener("url", (e) => onLink(e.url));
return () => sub.remove();
}, []);
}
Bu iki parça ölçülmez; model platformu ve çerçeveyi çalıştırmaz.
UK1. Model bağlantıyı siparis/<kimlik> biçiminde tek bir adres olarak kurar. openLink(routes, link) mevcut rota listesini ve adresi alır, yeni rota listesini döndürür. Adres tanınmıyorsa ya da hedef rota zaten yığının tepesindeyse liste olduğu gibi döner.
// link.mjs — derin baglantiyi rota listesine cevirir export function openLink(routes, link) { const parts = link.split("/"); if (parts[0] !== "siparis" || parts.length !== 2) return routes; const target = "siparis-" + parts[1]; if (routes[routes.length - 1] === target) return routes; return [...routes, target]; }
UK2. Yığının ağacını birinci dersteki gibi rotalardan kurarız: Stack altında her rota için anahtarlı bir Screen ve içinde bir Text.
// stack.mjs — gezinme yiginin rotalarindan gorunum agaci kurar import { el } from "./fate.mjs"; export function renderStack(routes) { return el("Stack", {}, routes.map((name) => el("Screen", { baslik: name }, [el("Text", { metin: name })], name))); }
Uygulama Açıkken
Kullanıcı sipariş listesinden bir siparişin ayrıntısına inmiş, sonra bildirim gelmiş. Yığında liste ve detay var. Bildirime dokunulunca uygulama zaten çalışıyor ve bağlantı olay olarak geliyor.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; import { openLink } from "./link.mjs"; const routes = ["liste", "detay"]; const before = renderStack(routes); const after = renderStack(openLink(routes, "siparis/42")); const ops = reconcile(before, after); printOps(ops); countFates(ops);
korunan: /Stack korunan: /Stack/Screen[liste] korunan: /Stack/Screen[liste]/Text korunan: /Stack/Screen[detay] korunan: /Stack/Screen[detay]/Text kurulan: /Stack/Screen[siparis-42] kurulan: /Stack/Screen[siparis-42]/Text korunan: 5 guncellenen: 0 kurulan: 2 sokulen: 0 komut: 2
/Stack/Screen[siparis-42] kurulandır ve onunla birlikte altındaki Text kurulur. Yığındaki öteki ekranlar ve içlerindeki metinler korunandır. Kullanıcı geri dönerse kaldığı yeri bulur; bağlantı yığını sıfırlamadı, üstüne bir ekran koydu.
Uygulama Kapalıyken
Uygulama kapalıyken bildirime dokunulursa bellekte ağaç yoktur. Platform uygulamayı başlatır ve ilk adres bağlantıdır. Başlangıç rotası liste, bağlantı hedefi siparis-42’dir; yığın ikisiyle birlikte kurulur.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; import { openLink } from "./link.mjs"; const start = openLink(["liste"], "siparis/42"); const ops = reconcile(null, renderStack(start)); printOps(ops); countFates(ops);
kurulan: /Stack kurulan: /Stack/Screen[liste] kurulan: /Stack/Screen[liste]/Text kurulan: /Stack/Screen[siparis-42] kurulan: /Stack/Screen[siparis-42]/Text korunan: 0 guncellenen: 0 kurulan: 5 sokulen: 0 komut: 5
Önceki ağaç olmadığı için ağacın tamamı kurulandır: yığın, iki ekran ve iki metin. Korunan düğüm yoktur, çünkü korunacak bir şey yoktur. Aynı bağlantı, uygulamanın açık ya da kapalı olmasına göre ağaçta iki ayrı iş yapar: açıkken tek bir ekran kurar, kapalıyken her şeyi. Bu iki ölçüm ayrı tutulmazsa bağlantının ağaca maliyeti yanlış anlaşılır.
Platform Parçalarını Satır Satır Okumak
NotificationTap işlevi, kullanıcının bildirime dokunduğu anı yakalar. İşlev bildirimin içindeki bir alandan adresi okur ve bir değişkene yazar; ekrana hiçbir şey çizmez ve yığına hiçbir rota eklemez. Son satır, platforma işin bittiğini bildirir. Bu işlevin görevi yalnız adresi bırakmaktır: adresi rotaya çevirmek ve yığına yazmak uygulamanın sonraki adımıdır.
Bu bölünme önemlidir. Platform kodu bağlantının geldiğini bilir ama hangi ekranın açılacağını bilmez; o bilgi rota tablosundadır. Rota tablosu ise bağlantının nereden geldiğini bilmez. Bu yüzden iki parça birbirinden bağımsız değişebilir: bildirimin biçimi değişince platform kodu, ekran adları değişince rota tablosu güncellenir.
LinkListener parçası, adresin JavaScript tarafına iki yoldan ulaştığını gösterir. İlk çağrı, uygulama bağlantıyla başlatıldıysa o ilk adresi sorar; adres yoksa hiçbir şey olmaz. İkinci çağrı, uygulama çalışırken gelen adresleri dinler. Temizleme işlevi (return () => sub.remove();) dinleyiciyi kaldırır; böylece ekran ömrü bittiğinde dinleme de biter. İki girişin ikisi de aynı işleve, onLink’e bağlanır. Model bu ortaklığı openLink ile yansıtır: açık ve kapalı yollar aynı dönüşümü kullanır, yalnız elde önceden bir yığın olup olmaması farklıdır.
Geri Dönmek
Bağlantıyla açılan ekranı kullanıcı kapatır. Yığından son rota çıkar ve bağlantının eklediği ekran ağaçtan gider. Bunu da ölçelim; çünkü bağlantı yığına bir ekran ekler ama bu ekranın ömrü bağlantıyla bitmez.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; const ops = reconcile( renderStack(["liste", "detay", "siparis-42"]), renderStack(["liste", "detay"]), ); printOps(ops.filter((op) => op.fate !== "korunan")); countFates(ops);
sokulen: /Stack/Screen[siparis-42] sokulen: /Stack/Screen[siparis-42]/Text korunan: 5 guncellenen: 0 kurulan: 0 sokulen: 2 komut: 2
Bağlantının kurduğu ekran ve içindeki metin sökülendir; geri kalan beş düğüm korunandır. Kuruluşun tersi olarak sökülme, kuruluşla aynı sayıda komut doğurur. Bağlantının ağaca maliyeti, ekranın yaşadığı süre boyunca bir kez ödenir ve ekran kapanınca bir kez daha: bir kurulum, bir söküm.
İkinci Bağlantı
Kullanıcı bir siparişe gitmiş, geri dönmeden ikinci bir bildirime dokunmuş olabilir. Yığında liste ve siparis-42 vardır, yeni bağlantı siparis/43 adresini taşır.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; import { openLink } from "./link.mjs"; const routes = ["liste", "siparis-42"]; const ops = reconcile(renderStack(routes), renderStack(openLink(routes, "siparis/43"))); printOps(ops.filter((op) => op.fate !== "korunan")); countFates(ops);
kurulan: /Stack/Screen[siparis-43] kurulan: /Stack/Screen[siparis-43]/Text korunan: 5 guncellenen: 0 kurulan: 2 sokulen: 0 komut: 2
İkinci bağlantı birincisini yerinden çıkarmadı; üstüne yeni bir ekran koydu. siparis-42 korunandır, siparis-43 kurulandır. Model bu seçimi UK1 ile yaptı: adres tanınıyorsa ve hedef tepede değilse rota eklenir. Gerçek bir uygulama bu durumda eski siparişi değiştirmeyi seçebilirdi; o hâlde ölçüm farklı bir akıbet verirdi ve bu ders o seçimi ölçmez. Ölçülen, modelin kurduğu davranıştır.
Rota Tablosu Ne Yapar
Rota tablosu, adresleri rota adlarına bağlayan küçük bir kural kümesidir. Modelimizde bu kural tek satırdır: siparis/<kimlik> biçimindeki adres siparis-<kimlik> rotasına dönüşür. Gerçek bir uygulamada tablo daha uzundur ve her satır bir ekranı, bazen de o ekranın parametrelerini tanımlar. Tablonun iki sınırı vardır. Tanımadığı adres için bir şey üretmez, ki bu ders bunu aşağıdaki taninmayan ölçümüyle gösterecek. Tanıdığı adres için ise yalnız rota adını üretir; ekranın içeriği, yani siparişin kendisi, ekran açıldıktan sonra ağ katmanından gelir.
Bu ikinci sınır ağaç açısından önemlidir. Bağlantı ekranı kurar, ama siparişin ayrıntıları henüz yoktur. Ekran önce yükleniyor hâlinde açılır, sonra veri gelince güncellenir. Bağlantının ağaca maliyeti bu yüzden iki aşamalıdır: ekranın kurulması ve içeriğin gelmesi. Bu derste yalnız ilk aşama ölçüldü; ikinci aşama Ağ ve Veri dersinin konusudur.
Başlangıç Sırası
Uygulama bağlantıyla başlatıldığında iki yol vardır. Birincisinde uygulama ilk adresi sorar ve ağacı doğrudan bağlantının gösterdiği yığınla kurar; ölçümümüz bu yolu gösterdi ve beş düğümün beşi kurulan oldu. İkincisinde uygulama önce başlangıç ekranını kurar, adresi sonra öğrenir ve bağlantıyı açık uygulamaya gelen bir olay gibi işler. Bu yolda başlangıç yığınının üç düğümü (Stack, liste ekranı ve metni) önce kurulur, bağlantının iki düğümü sonra eklenir; toplam yine beş kurulan düğümdür, ama kullanıcı kısa bir an için yalnız başlangıç ekranını görür.
İki yol komut sayısı bakımından eşittir. Aralarındaki fark ağacın değil, kullanıcının gördüğü ara durumun farkıdır. Kural ara durumu görmez; yalnız karşılaştırılan iki ağacı görür. Bu yüzden bu iki yolun karşılaştırılması ağaç akıbetinin dışında, bir tasarım değerlendirmesidir.
Sınırlayıcı Ölçüm
Bağlantının uygulamaya ulaşması ağacın değişmesi demek değildir. Rota tablosu adresi tanımayabilir, ya da bildirim iki kez gelebilir. İki durumda da bağlantı işlenmiştir ama ağaç değişmez.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; import { openLink } from "./link.mjs"; function measure(label, routes, link) { console.log(label); countFates(reconcile(renderStack(routes), renderStack(openLink(routes, link)))); } measure("taninmayan:", ["liste", "detay"], "kampanya/7"); measure("yinelenen:", ["liste", "siparis-42"], "siparis/42");
taninmayan: korunan: 5 guncellenen: 0 kurulan: 0 sokulen: 0 komut: 0 yinelenen: korunan: 5 guncellenen: 0 kurulan: 0 sokulen: 0 komut: 0
İki ölçümde de komut 0’dır: tüm düğümler korunandır. Akıbet bağlantının işlenip işlenmediğini ölçmez; ağacın ne olduğunu ölçer. Bildirime dokunulmuş, uygulama bağlantıyı almış ve rota tablosu karar vermiş olabilir; ağaçta hiçbir iz kalmayabilir.
Yaygın Yanılgılar
- “Bağlantı geldi, demek ki yeni ekran açıldı.” Ekran, rota listesine bir rota eklenirse açılır. Tanınmayan ya da yinelenen bağlantı listeyi değiştirmez.
- “Bildirim uygulamayı hep sıfırdan başlatır.” Uygulama açıksa bağlantı olay olarak gelir ve yığın korunur; sıfırdan kurulum yalnız uygulama kapalıyken olur.
- “Bildirimin kendisi ağaçta görünür.” Bildirim işletim sisteminin bildirim alanında durur; ağaca yalnız ona dokunulunca gelen bağlantı girer.
- “Aynı bildirim iki kez gelirse iki ekran açılır.” Hedef rota zaten tepedeyse model listeyi değiştirmez. Yinelenen bağlantı ölçümü komut 0 verdi.
- “Tanınmayan bağlantı hata verir.” Model tanınmayan adreste listeyi olduğu gibi döndürür ve ağaç değişmez. Kullanıcıya bir şey söylemek isteniyorsa bu ayrı bir düğüm olarak ağaca yazılmalıdır; o zaman ölçüm komut 0 yerine bir kurulan düğüm gösterirdi.
- “Bağlantı yığını her zaman büyütür.” Bağlantı yığına bir rota ekleyebilir, hiçbir şey eklemeyebilir ya da bir uygulama yığını baştan kurmayı seçebilir. Bu ders yalnız ekleme davranışını ölçtü.
- “Bildirim izni bağlantıyı ağaçta gösterir.” İzin bildirimin gösterilip gösterilmeyeceğini belirler. Gösterilen bildirime dokunulmadıkça uygulamaya bağlantı ulaşmaz ve ağaç değişmez.
- “Aynı bağlantı her zaman aynı ağacı verir.” Sonuç, bağlantının geldiği andaki yığına bağlıdır. Aynı
siparis/42adresi uygulama kapalıyken, önceki ağaç yokken beş,listevedetayekranlı yığında iki kurulan düğüm üretti. Bağlantının maliyetini yalnız adrese bakarak söylemek mümkün değildir.
Özet
- Bu dersin akıbeti: kurulan. Uygulama açıkken bağlantı
/Stack/Screen[siparis-42]ekranını kurar, alttaki ekranlar korunandır. - Uygulama kapalıyken önceki ağaç yoktur; bağlantı ağacın tamamını kurulan yapar.
- Aynı bağlantı açıkken tek bir ekran, kapalıyken tüm ağaç kurar; iki durum ayrı ölçülür.
- Tanınmayan ve yinelenen bağlantı ağaçta komut doğurmaz, bağlantının işlenmiş olması ağacın değiştiği anlamına gelmez.
- Bağlantıyı adres olarak taşıyan platform koduyla onu rotaya çeviren rota tablosu ayrı iki iştir; akıbeti ikincisinin sonucu belirler.
Sonraki Adım
Bu derse kadar ağaç kesikli adımlarla değişti: bir dokunuş, bir yanıt, bir bağlantı. Parmak ekranda sürüklenirken değişiklik her karede olur. Son ders, Hareketler ve Animasyonlar, aynı hareketin ağaca iki yoldan yazılması hâlinde komutun nasıl ayrıştığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.